Browse Source

add net performance to current positions #324

pull/339/head
Valentin Zickner 4 years ago
parent
commit
3a14df7c00
  1. 2
      apps/api/src/app/portfolio/interfaces/current-positions.interface.ts
  2. 594
      apps/api/src/app/portfolio/portfolio-calculator.spec.ts
  3. 61
      apps/api/src/app/portfolio/portfolio-calculator.ts
  4. 2
      libs/common/src/lib/interfaces/timeline-position.interface.ts

2
apps/api/src/app/portfolio/interfaces/current-positions.interface.ts

@ -6,6 +6,8 @@ export interface CurrentPositions {
positions: TimelinePosition[]; positions: TimelinePosition[];
grossPerformance: Big; grossPerformance: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;
netPerformance: Big;
netPerformancePercentage: Big;
currentValue: Big; currentValue: Big;
totalInvestment: Big; totalInvestment: Big;
} }

594
apps/api/src/app/portfolio/portfolio-calculator.spec.ts

@ -712,27 +712,29 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('657.62'), hasErrors: false,
grossPerformance: new Big('-61.84'), currentValue: new Big('657.62'),
grossPerformancePercentage: new Big('-0.08595335390431712673'), grossPerformance: new Big('-61.84'),
totalInvestment: new Big('719.46'), grossPerformancePercentage: new Big('-0.08595335390431712673'),
positions: [ totalInvestment: new Big('719.46'),
{ positions: [
averagePrice: new Big('719.46'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('719.46'),
firstBuyDate: '2021-01-01', currency: 'USD',
grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 firstBuyDate: '2021-01-01',
grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84
investment: new Big('719.46'), grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673
marketPrice: 657.62, investment: new Big('719.46'),
quantity: new Big('1'), marketPrice: 657.62,
symbol: 'TSLA', quantity: new Big('1'),
transactionCount: 1 symbol: 'TSLA',
} transactionCount: 1
] })
}); ]
})
);
}); });
it('with single TSLA and buy day start', async () => { it('with single TSLA and buy day start', async () => {
@ -750,27 +752,29 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('657.62'), hasErrors: false,
grossPerformance: new Big('-61.84'), currentValue: new Big('657.62'),
grossPerformancePercentage: new Big('-0.08595335390431712673'), grossPerformance: new Big('-61.84'),
totalInvestment: new Big('719.46'), grossPerformancePercentage: new Big('-0.08595335390431712673'),
positions: [ totalInvestment: new Big('719.46'),
{ positions: [
averagePrice: new Big('719.46'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('719.46'),
firstBuyDate: '2021-01-01', currency: 'USD',
grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 firstBuyDate: '2021-01-01',
grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84
investment: new Big('719.46'), grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673
marketPrice: 657.62, investment: new Big('719.46'),
quantity: new Big('1'), marketPrice: 657.62,
symbol: 'TSLA', quantity: new Big('1'),
transactionCount: 1 symbol: 'TSLA',
} transactionCount: 1
] })
}); ]
})
);
}); });
it('with single TSLA and late start', async () => { it('with single TSLA and late start', async () => {
@ -788,27 +792,29 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('657.62'), hasErrors: false,
grossPerformance: new Big('-9.04'), currentValue: new Big('657.62'),
grossPerformancePercentage: new Big('-0.01356013560135601356'), grossPerformance: new Big('-9.04'),
totalInvestment: new Big('719.46'), grossPerformancePercentage: new Big('-0.01356013560135601356'),
positions: [ totalInvestment: new Big('719.46'),
{ positions: [
averagePrice: new Big('719.46'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('719.46'),
firstBuyDate: '2021-01-01', currency: 'USD',
grossPerformance: new Big('-9.04'), // 657.62-666.66=-9.04 firstBuyDate: '2021-01-01',
grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136 grossPerformance: new Big('-9.04'), // 657.62-666.66=-9.04
investment: new Big('719.46'), grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136
marketPrice: 657.62, investment: new Big('719.46'),
quantity: new Big('1'), marketPrice: 657.62,
symbol: 'TSLA', quantity: new Big('1'),
transactionCount: 1 symbol: 'TSLA',
} transactionCount: 1
] })
}); ]
})
);
}); });
it('with VTI only', async () => { it('with VTI only', async () => {
@ -826,30 +832,32 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('4871.5'), hasErrors: false,
grossPerformance: new Big('240.4'), currentValue: new Big('4871.5'),
grossPerformancePercentage: new Big('0.08839407904876477102'), grossPerformance: new Big('240.4'),
totalInvestment: new Big('4460.95'), grossPerformancePercentage: new Big('0.08839407904876477102'),
positions: [ totalInvestment: new Big('4460.95'),
{ positions: [
averagePrice: new Big('178.438'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('178.438'),
firstBuyDate: '2019-02-01', currency: 'USD',
// see next test for details about how to calculate this firstBuyDate: '2019-02-01',
grossPerformance: new Big('240.4'), // see next test for details about how to calculate this
grossPerformancePercentage: new Big( grossPerformance: new Big('240.4'),
'0.0883940790487647710162214425767848424215253864940558186258745429269647266073266478435285352186572448' grossPerformancePercentage: new Big(
), '0.0883940790487647710162214425767848424215253864940558186258745429269647266073266478435285352186572448'
investment: new Big('4460.95'), ),
marketPrice: 194.86, investment: new Big('4460.95'),
quantity: new Big('25'), marketPrice: 194.86,
symbol: 'VTI', quantity: new Big('25'),
transactionCount: 5 symbol: 'VTI',
} transactionCount: 5
] })
}); ]
})
);
}); });
it('with buy and sell', async () => { it('with buy and sell', async () => {
@ -867,41 +875,43 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('4871.5'), hasErrors: false,
grossPerformance: new Big('240.4'), currentValue: new Big('4871.5'),
grossPerformancePercentage: new Big('0.01104605615757711361'), grossPerformance: new Big('240.4'),
totalInvestment: new Big('4460.95'), grossPerformancePercentage: new Big('0.01104605615757711361'),
positions: [ totalInvestment: new Big('4460.95'),
{ positions: [
averagePrice: new Big('0'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('0'),
firstBuyDate: '2019-09-01', currency: 'USD',
grossPerformance: new Big('0'), firstBuyDate: '2019-09-01',
grossPerformancePercentage: new Big('0'), grossPerformance: new Big('0'),
investment: new Big('0'), grossPerformancePercentage: new Big('0'),
marketPrice: 2021.99, investment: new Big('0'),
quantity: new Big('0'), marketPrice: 2021.99,
symbol: 'AMZN', quantity: new Big('0'),
transactionCount: 2 symbol: 'AMZN',
}, transactionCount: 2
{ }),
averagePrice: new Big('178.438'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('178.438'),
firstBuyDate: '2019-02-01', currency: 'USD',
grossPerformance: new Big('240.4'), firstBuyDate: '2019-02-01',
grossPerformancePercentage: new Big( grossPerformance: new Big('240.4'),
'0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552' grossPerformancePercentage: new Big(
), '0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552'
investment: new Big('4460.95'), ),
marketPrice: 194.86, investment: new Big('4460.95'),
quantity: new Big('25'), marketPrice: 194.86,
symbol: 'VTI', quantity: new Big('25'),
transactionCount: 5 symbol: 'VTI',
} transactionCount: 5
] })
}); ]
})
);
}); });
it('with buy, sell, buy', async () => { it('with buy, sell, buy', async () => {
@ -962,29 +972,31 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('1086.7'), hasErrors: false,
grossPerformance: new Big('207.6'), currentValue: new Big('1086.7'),
grossPerformancePercentage: new Big('0.2516103956224511062'), grossPerformance: new Big('207.6'),
totalInvestment: new Big('1013.9'), grossPerformancePercentage: new Big('0.2516103956224511062'),
positions: [ totalInvestment: new Big('1013.9'),
{ positions: [
averagePrice: new Big('202.78'), expect.objectContaining({
currency: 'USD', averagePrice: new Big('202.78'),
firstBuyDate: '2019-09-01', currency: 'USD',
grossPerformance: new Big('207.6'), firstBuyDate: '2019-09-01',
grossPerformancePercentage: new Big( grossPerformance: new Big('207.6'),
'0.2516103956224511061954915466429950404846' grossPerformancePercentage: new Big(
), '0.2516103956224511061954915466429950404846'
investment: new Big('1013.9'), ),
marketPrice: 217.34, investment: new Big('1013.9'),
quantity: new Big('5'), marketPrice: 217.34,
symbol: 'VTI', quantity: new Big('5'),
transactionCount: 3 symbol: 'VTI',
} transactionCount: 3
] })
}); ]
})
);
}); });
it('with performance since Jan 1st, 2020', async () => { it('with performance since Jan 1st, 2020', async () => {
@ -1042,12 +1054,101 @@ describe('PortfolioCalculator', () => {
parseDate('2020-01-01') parseDate('2020-01-01')
); );
spy.mockRestore();
expect(currentPositions).toEqual(
expect.objectContaining({
hasErrors: false,
currentValue: new Big('3897.2'),
grossPerformance: new Big('303.2'),
grossPerformancePercentage: new Big('0.27537838148272398344'),
totalInvestment: new Big('2923.7'),
positions: [
expect.objectContaining({
averagePrice: new Big('146.185'),
firstBuyDate: '2019-02-01',
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
marketPrice: 194.86,
transactionCount: 2,
grossPerformance: new Big('303.2'),
grossPerformancePercentage: new Big(
'0.2753783814827239834392742298083677500037'
),
currency: 'USD'
})
]
})
);
});
it('with net performance since Jan 1st, 2020 - include fees', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
);
const transactionPoints = [
{
date: '2019-02-01',
items: [
{
quantity: new Big('10'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
fee: new Big(50),
transactionCount: 1
}
]
},
{
date: '2020-08-03',
items: [
{
quantity: new Big('20'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
fee: new Big(50),
transactionCount: 2
}
]
}
];
portfolioCalculator.setTransactionPoints(transactionPoints);
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24
// 2020-01-01 -> days 334 => value: VTI: 144.38+334*0.08=171.1 => 10*171.10=1711
// 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 10*188.30=1883 => 1883/1711 = 1.100526008
// 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 20*188.30=3766
// cash flow: 2923.7-1443.8=1479.9
// 2020-10-24 [today] -> days 631 => value: VTI: 144.38+631*0.08=194.86 => 20*194.86=3897.2 => 3897.2/(1883+1479.9) = 1.158880728
// and net: 3897.2/(1883+1479.9+50) = 1.14190278
// gross performance: 1883-1711 + 3897.2-3766 = 303.2
// gross performance percentage: 1.100526008 * 1.158880728 = 1.275378381 => 27.5378381 %
// net performance percentage: 1.100526008 * 1.14190278 = 1.25669371 => 25.669371 %
// more details: https://github.com/ghostfolio/ghostfolio/issues/324#issuecomment-910530823
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2020-01-01')
);
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
hasErrors: false, hasErrors: false,
currentValue: new Big('3897.2'), currentValue: new Big('3897.2'),
grossPerformance: new Big('303.2'), grossPerformance: new Big('303.2'),
grossPerformancePercentage: new Big('0.27537838148272398344'), grossPerformancePercentage: new Big('0.27537838148272398344'),
netPerformance: new Big('253.2'),
netPerformancePercentage: new Big('0.2566937088951485493'),
totalInvestment: new Big('2923.7'), totalInvestment: new Big('2923.7'),
positions: [ positions: [
{ {
@ -1062,12 +1163,101 @@ describe('PortfolioCalculator', () => {
grossPerformancePercentage: new Big( grossPerformancePercentage: new Big(
'0.2753783814827239834392742298083677500037' '0.2753783814827239834392742298083677500037'
), ),
netPerformance: new Big('253.2'), // gross - 50 fees
netPerformancePercentage: new Big(
'0.2566937088951485493029975263687800261527'
), // see details above
currency: 'USD' currency: 'USD'
} }
] ]
}); });
}); });
it('with net performance since Feb 1st, 2019 - include fees', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
);
const transactionPoints = [
{
date: '2019-02-01',
items: [
{
quantity: new Big('10'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
fee: new Big(50),
transactionCount: 1
}
]
},
{
date: '2020-08-03',
items: [
{
quantity: new Big('20'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
fee: new Big(50),
transactionCount: 2
}
]
}
];
portfolioCalculator.setTransactionPoints(transactionPoints);
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24
// 2019-02-01 -> value: VTI: 1443.8
// 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 10*188.30=1883 => net: 1883/(1443.8+50) = 1.26054358
// 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 20*188.30=3766
// cash flow: 2923.7-1443.8=1479.9
// 2020-10-24 [today] -> days 631 => value: VTI: 144.38+631*0.08=194.86 => 20*194.86=3897.2 => net: 3897.2/(1883+1479.9+50) = 1.14190278
// gross performance: 1883-1443.8 + 3897.2-3766 = 570.4 => net performance: 470.4
// net performance percentage: 1.26054358 * 1.14190278 = 1.43941822 => 43.941822 %
// more details: https://github.com/ghostfolio/ghostfolio/issues/324#issuecomment-910530823
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2019-02-01')
);
spy.mockRestore();
expect(currentPositions).toEqual(
expect.objectContaining({
hasErrors: false,
currentValue: new Big('3897.2'),
netPerformance: new Big('470.4'),
netPerformancePercentage: new Big('0.4394182192526437059'),
totalInvestment: new Big('2923.7'),
positions: [
expect.objectContaining({
averagePrice: new Big('146.185'),
firstBuyDate: '2019-02-01',
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
marketPrice: 194.86,
transactionCount: 2,
netPerformance: new Big('470.4'),
netPerformancePercentage: new Big(
'0.4394182192526437058970248283134805555953'
), // see details above
currency: 'USD'
})
]
})
);
});
/** /**
* Source: https://www.investopedia.com/terms/t/time-weightedror.asp * Source: https://www.investopedia.com/terms/t/time-weightedror.asp
*/ */
@ -1116,27 +1306,31 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
hasErrors: false, expect.objectContaining({
currentValue: new Big('1192327.999656600298238721'), hasErrors: false,
grossPerformance: new Big('92327.999656600898394721'), currentValue: new Big('1192327.999656600298238721'),
grossPerformancePercentage: new Big('0.09788498099999947809'), grossPerformance: new Big('92327.999656600898394721'),
totalInvestment: new Big('1100000'), grossPerformancePercentage: new Big('0.09788498099999947809'),
positions: [ totalInvestment: new Big('1100000'),
{ positions: [
averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542 expect.objectContaining({
firstBuyDate: '2010-12-31', averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542
quantity: new Big('1086022.689344541'), firstBuyDate: '2010-12-31',
symbol: 'MFA', quantity: new Big('1086022.689344541'),
investment: new Big('1100000'), symbol: 'MFA',
marketPrice: 1.097884981, investment: new Big('1100000'),
transactionCount: 2, marketPrice: 1.097884981,
grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328 transactionCount: 2,
grossPerformancePercentage: new Big('0.09788498099999947808927632'), // 9.79 % grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328
currency: 'USD' grossPerformancePercentage: new Big(
} '0.09788498099999947808927632'
] ), // 9.79 %
}); currency: 'USD'
})
]
})
);
}); });
/** /**
@ -1205,39 +1399,41 @@ describe('PortfolioCalculator', () => {
); );
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual(
currentValue: new Big('517'), expect.objectContaining({
grossPerformance: new Big('17'), // 517 - 500 currentValue: new Big('517'),
grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4% grossPerformance: new Big('17'), // 517 - 500
totalInvestment: new Big('500'), grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4%
hasErrors: false, totalInvestment: new Big('500'),
positions: [ hasErrors: false,
{ positions: [
averagePrice: new Big('1'), expect.objectContaining({
firstBuyDate: '2012-12-31', averagePrice: new Big('1'),
quantity: new Big('200'), firstBuyDate: '2012-12-31',
symbol: 'SPA', quantity: new Big('200'),
investment: new Big('200'), symbol: 'SPA',
marketPrice: 1.025, // 205 / 200 investment: new Big('200'),
transactionCount: 1, marketPrice: 1.025, // 205 / 200
grossPerformance: new Big('5'), // 205 - 200 transactionCount: 1,
grossPerformancePercentage: new Big('0.025'), grossPerformance: new Big('5'), // 205 - 200
currency: 'CHF' grossPerformancePercentage: new Big('0.025'),
}, currency: 'CHF'
{ }),
averagePrice: new Big('1'), expect.objectContaining({
firstBuyDate: '2012-12-31', averagePrice: new Big('1'),
quantity: new Big('300'), firstBuyDate: '2012-12-31',
symbol: 'SPB', quantity: new Big('300'),
investment: new Big('300'), symbol: 'SPB',
marketPrice: 1.04, // 312 / 300 investment: new Big('300'),
transactionCount: 1, marketPrice: 1.04, // 312 / 300
grossPerformance: new Big('12'), // 312 - 300 transactionCount: 1,
grossPerformancePercentage: new Big('0.04'), grossPerformance: new Big('12'), // 312 - 300
currency: 'CHF' grossPerformancePercentage: new Big('0.04'),
} currency: 'CHF'
] })
}); ]
})
);
}); });
}); });

61
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -123,6 +123,8 @@ export class PortfolioCalculator {
positions: [], positions: [],
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
currentValue: new Big(0), currentValue: new Big(0),
totalInvestment: new Big(0) totalInvestment: new Big(0)
}; };
@ -188,7 +190,9 @@ export class PortfolioCalculator {
const startString = format(start, DATE_FORMAT); const startString = format(start, DATE_FORMAT);
const holdingPeriodReturns: { [symbol: string]: Big } = {}; const holdingPeriodReturns: { [symbol: string]: Big } = {};
const netHoldingPeriodReturns: { [symbol: string]: Big } = {};
const grossPerformance: { [symbol: string]: Big } = {}; const grossPerformance: { [symbol: string]: Big } = {};
const netPerformance: { [symbol: string]: Big } = {};
const todayString = format(today, DATE_FORMAT); const todayString = format(today, DATE_FORMAT);
if (firstIndex > 0) { if (firstIndex > 0) {
@ -209,10 +213,6 @@ export class PortfolioCalculator {
const items = this.transactionPoints[i].items; const items = this.transactionPoints[i].items;
for (const item of items) { for (const item of items) {
let oldHoldingPeriodReturn = holdingPeriodReturns[item.symbol];
if (!oldHoldingPeriodReturn) {
oldHoldingPeriodReturn = new Big(1);
}
if (!marketSymbolMap[nextDate]?.[item.symbol]) { if (!marketSymbolMap[nextDate]?.[item.symbol]) {
invalidSymbols.push(item.symbol); invalidSymbols.push(item.symbol);
hasErrors = true; hasErrors = true;
@ -231,6 +231,12 @@ export class PortfolioCalculator {
const itemValue = marketSymbolMap[currentDate]?.[item.symbol]; const itemValue = marketSymbolMap[currentDate]?.[item.symbol];
let initialValue = itemValue?.mul(lastQuantity); let initialValue = itemValue?.mul(lastQuantity);
let investedValue = itemValue?.mul(item.quantity); let investedValue = itemValue?.mul(item.quantity);
const isFirstOrderAndIsStartBeforeCurrentDate =
i === firstIndex &&
isBefore(parseDate(this.transactionPoints[i].date), start);
const fee = isFirstOrderAndIsStartBeforeCurrentDate
? new Big(0)
: item.fee;
if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) { if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) {
initialValue = item.investment; initialValue = item.investment;
investedValue = item.investment; investedValue = item.investment;
@ -254,15 +260,22 @@ export class PortfolioCalculator {
); );
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow)); const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
holdingPeriodReturns[item.symbol] = holdingPeriodReturns[item.symbol] = (
oldHoldingPeriodReturn.mul(holdingPeriodReturn); holdingPeriodReturns[item.symbol] ?? new Big(1)
let oldGrossPerformance = grossPerformance[item.symbol]; ).mul(holdingPeriodReturn);
if (!oldGrossPerformance) { grossPerformance[item.symbol] = (
oldGrossPerformance = new Big(0); grossPerformance[item.symbol] ?? new Big(0)
} ).plus(endValue.minus(investedValue));
const currentPerformance = endValue.minus(investedValue);
grossPerformance[item.symbol] = const netHoldingPeriodReturn = endValue.div(
oldGrossPerformance.plus(currentPerformance); initialValue.plus(cashFlow).plus(fee)
);
netHoldingPeriodReturns[item.symbol] = (
netHoldingPeriodReturns[item.symbol] ?? new Big(1)
).mul(netHoldingPeriodReturn);
netPerformance[item.symbol] = (
netPerformance[item.symbol] ?? new Big(0)
).plus(endValue.minus(investedValue).minus(fee));
} }
lastInvestments[item.symbol] = item.investment; lastInvestments[item.symbol] = item.investment;
lastQuantities[item.symbol] = item.quantity; lastQuantities[item.symbol] = item.quantity;
@ -287,6 +300,11 @@ export class PortfolioCalculator {
isValid && holdingPeriodReturns[item.symbol] isValid && holdingPeriodReturns[item.symbol]
? holdingPeriodReturns[item.symbol].minus(1) ? holdingPeriodReturns[item.symbol].minus(1)
: null, : null,
netPerformance: isValid ? netPerformance[item.symbol] ?? null : null,
netPerformancePercentage:
isValid && netHoldingPeriodReturns[item.symbol]
? netHoldingPeriodReturns[item.symbol].minus(1)
: null,
investment: item.investment, investment: item.investment,
marketPrice: marketValue?.toNumber() ?? null, marketPrice: marketValue?.toNumber() ?? null,
quantity: item.quantity, quantity: item.quantity,
@ -294,10 +312,7 @@ export class PortfolioCalculator {
transactionCount: item.transactionCount transactionCount: item.transactionCount
}); });
} }
const overall = this.calculateOverallGrossPerformance( const overall = this.calculateOverallPerformance(positions, initialValues);
positions,
initialValues
);
return { return {
...overall, ...overall,
@ -385,7 +400,7 @@ export class PortfolioCalculator {
return flatten(timelinePeriods); return flatten(timelinePeriods);
} }
private calculateOverallGrossPerformance( private calculateOverallPerformance(
positions: TimelinePosition[], positions: TimelinePosition[],
initialValues: { [p: string]: Big } initialValues: { [p: string]: Big }
) { ) {
@ -394,6 +409,8 @@ export class PortfolioCalculator {
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
let grossPerformancePercentage = new Big(0); let grossPerformancePercentage = new Big(0);
let netPerformance = new Big(0);
let netPerformancePercentage = new Big(0);
let completeInitialValue = new Big(0); let completeInitialValue = new Big(0);
for (const currentPosition of positions) { for (const currentPosition of positions) {
if (currentPosition.marketPrice) { if (currentPosition.marketPrice) {
@ -408,6 +425,7 @@ export class PortfolioCalculator {
grossPerformance = grossPerformance.plus( grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance currentPosition.grossPerformance
); );
netPerformance = netPerformance.plus(currentPosition.netPerformance);
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
hasErrors = true; hasErrors = true;
} }
@ -421,6 +439,9 @@ export class PortfolioCalculator {
grossPerformancePercentage = grossPerformancePercentage.plus( grossPerformancePercentage = grossPerformancePercentage.plus(
currentPosition.grossPerformancePercentage.mul(currentInitialValue) currentPosition.grossPerformancePercentage.mul(currentInitialValue)
); );
netPerformancePercentage = netPerformancePercentage.plus(
currentPosition.netPerformancePercentage.mul(currentInitialValue)
);
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
console.error( console.error(
`Initial value is missing for symbol ${currentPosition.symbol}` `Initial value is missing for symbol ${currentPosition.symbol}`
@ -432,12 +453,16 @@ export class PortfolioCalculator {
if (!completeInitialValue.eq(0)) { if (!completeInitialValue.eq(0)) {
grossPerformancePercentage = grossPerformancePercentage =
grossPerformancePercentage.div(completeInitialValue); grossPerformancePercentage.div(completeInitialValue);
netPerformancePercentage =
netPerformancePercentage.div(completeInitialValue);
} }
return { return {
currentValue, currentValue,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
netPerformance,
netPerformancePercentage,
hasErrors, hasErrors,
totalInvestment totalInvestment
}; };

2
libs/common/src/lib/interfaces/timeline-position.interface.ts

@ -7,6 +7,8 @@ export interface TimelinePosition {
firstBuyDate: string; firstBuyDate: string;
grossPerformance: Big; grossPerformance: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;
netPerformance: Big;
netPerformancePercentage: Big;
investment: Big; investment: Big;
marketPrice: number; marketPrice: number;
quantity: Big; quantity: Big;

Loading…
Cancel
Save