diff --git a/CHANGELOG.md b/CHANGELOG.md index bcac8423b..4d76a8a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a story for the trend indicator component - Added a story for the value component +### Changed + +- Switched from gross to net performance +- Restructured the portfolio summary tab on the home page (fees and net performance) + ## 1.45.0 - 04.09.2021 ### Added diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts index 90fcf9b01..e855f7e76 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -6,6 +6,8 @@ export interface CurrentPositions { positions: TimelinePosition[]; grossPerformance: Big; grossPerformancePercentage: Big; + netPerformance: Big; + netPerformancePercentage: Big; currentValue: Big; totalInvestment: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index b3dd6d0a3..2b443236e 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -5,6 +5,7 @@ import Big from 'big.js'; export interface PortfolioOrder { currency: Currency; date: string; + fee: Big; name: string; quantity: Big; symbol: string; diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 34055d40e..b7836e78f 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -11,6 +11,8 @@ export interface PortfolioPositionDetail { marketPrice: number; maxPrice: number; minPrice: number; + netPerformance: number; + netPerformancePercent: number; quantity: number; symbol: string; transactionCount: number; diff --git a/apps/api/src/app/portfolio/interfaces/timeline-period.interface.ts b/apps/api/src/app/portfolio/interfaces/timeline-period.interface.ts index a26bc0c15..0031d50d3 100644 --- a/apps/api/src/app/portfolio/interfaces/timeline-period.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/timeline-period.interface.ts @@ -4,5 +4,6 @@ export interface TimelinePeriod { date: string; grossPerformance: Big; investment: Big; + netPerformance: Big; value: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts index 0cf66083c..91dcdd63b 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts @@ -3,6 +3,7 @@ import Big from 'big.js'; export interface TransactionPointSymbol { currency: Currency; + fee: Big; firstBuyDate: string; investment: Big; quantity: Big; diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 9bf57bb27..440139c8f 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -142,6 +142,198 @@ describe('PortfolioCalculator', () => { ); }); + it('with orders of only one symbol and a fee', () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + const orders = [ + { + date: '2019-02-01', + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + quantity: new Big('10'), + symbol: 'VTI', + type: OrderType.Buy, + unitPrice: new Big('144.38'), + currency: Currency.USD, + fee: new Big('5') + }, + { + date: '2019-08-03', + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + quantity: new Big('10'), + symbol: 'VTI', + type: OrderType.Buy, + unitPrice: new Big('147.99'), + currency: Currency.USD, + fee: new Big('10') + }, + { + date: '2020-02-02', + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + quantity: new Big('15'), + symbol: 'VTI', + type: OrderType.Sell, + unitPrice: new Big('151.41'), + currency: Currency.USD, + fee: new Big('5') + } + ]; + portfolioCalculator.computeTransactionPoints(orders); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); + + expect(portfolioItemsAtTransactionPoints).toEqual([ + { + date: '2019-02-01', + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1, + fee: new Big('5') + } + ] + }, + { + date: '2019-08-03', + items: [ + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2, + fee: new Big('15') + } + ] + }, + { + date: '2020-02-02', + items: [ + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3, + fee: new Big('20') + } + ] + } + ]); + }); + + it('with orders of two different symbols and a fee', () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + const orders = [ + { + date: '2019-02-01', + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + quantity: new Big('10'), + symbol: 'VTI', + type: OrderType.Buy, + unitPrice: new Big('144.38'), + currency: Currency.USD, + fee: new Big('5') + }, + { + date: '2019-08-03', + name: 'Something else', + quantity: new Big('10'), + symbol: 'VTX', + type: OrderType.Buy, + unitPrice: new Big('147.99'), + currency: Currency.USD, + fee: new Big('10') + }, + { + date: '2020-02-02', + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + quantity: new Big('5'), + symbol: 'VTI', + type: OrderType.Sell, + unitPrice: new Big('151.41'), + currency: Currency.USD, + fee: new Big('5') + } + ]; + portfolioCalculator.computeTransactionPoints(orders); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); + + expect(portfolioItemsAtTransactionPoints).toEqual([ + { + date: '2019-02-01', + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1, + fee: new Big('5') + } + ] + }, + { + date: '2019-08-03', + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1, + fee: new Big('5') + }, + { + quantity: new Big('10'), + symbol: 'VTX', + investment: new Big('1479.9'), + currency: Currency.USD, + firstBuyDate: '2019-08-03', + transactionCount: 1, + fee: new Big('10') + } + ] + }, + { + date: '2020-02-02', + items: [ + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('686.75'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2, + fee: new Big('10') + }, + { + quantity: new Big('10'), + symbol: 'VTX', + investment: new Big('1479.9'), + currency: Currency.USD, + firstBuyDate: '2019-08-03', + transactionCount: 1, + fee: new Big('10') + } + ] + } + ]); + }); + it('with two orders at the same day of the same type', () => { const orders = [ ...ordersVTI, @@ -152,7 +344,8 @@ describe('PortfolioCalculator', () => { quantity: new Big('20'), symbol: 'VTI', type: OrderType.Buy, - unitPrice: new Big('197.15') + unitPrice: new Big('197.15'), + fee: new Big(0) } ]; const portfolioCalculator = new PortfolioCalculator( @@ -173,6 +366,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1443.8'), quantity: new Big('10'), symbol: 'VTI', + fee: new Big(0), transactionCount: 1 } ] @@ -186,6 +380,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2923.7'), quantity: new Big('20'), symbol: 'VTI', + fee: new Big(0), transactionCount: 2 } ] @@ -199,6 +394,7 @@ describe('PortfolioCalculator', () => { investment: new Big('652.55'), quantity: new Big('5'), symbol: 'VTI', + fee: new Big(0), transactionCount: 3 } ] @@ -212,6 +408,7 @@ describe('PortfolioCalculator', () => { investment: new Big('6627.05'), quantity: new Big('35'), symbol: 'VTI', + fee: new Big(0), transactionCount: 5 } ] @@ -225,6 +422,7 @@ describe('PortfolioCalculator', () => { investment: new Big('8403.95'), quantity: new Big('45'), symbol: 'VTI', + fee: new Big(0), transactionCount: 6 } ] @@ -242,7 +440,8 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', type: OrderType.Buy, - unitPrice: new Big('2021.99') + unitPrice: new Big('2021.99'), + fee: new Big(0) } ]; const portfolioCalculator = new PortfolioCalculator( @@ -263,6 +462,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1443.8'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 1 } ] @@ -276,6 +476,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2923.7'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 2 } ] @@ -289,6 +490,7 @@ describe('PortfolioCalculator', () => { investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -297,6 +499,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2923.7'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 2 } ] @@ -310,6 +513,7 @@ describe('PortfolioCalculator', () => { investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -318,6 +522,7 @@ describe('PortfolioCalculator', () => { investment: new Big('652.55'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 3 } ] @@ -331,6 +536,7 @@ describe('PortfolioCalculator', () => { investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -339,6 +545,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2684.05'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 4 } ] @@ -352,6 +559,7 @@ describe('PortfolioCalculator', () => { investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -360,6 +568,7 @@ describe('PortfolioCalculator', () => { investment: new Big('4460.95'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 5 } ] @@ -377,7 +586,8 @@ describe('PortfolioCalculator', () => { symbol: 'AMZN', type: OrderType.Buy, unitPrice: new Big('2021.99'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2020-08-02', @@ -386,7 +596,8 @@ describe('PortfolioCalculator', () => { symbol: 'AMZN', type: OrderType.Sell, unitPrice: new Big('2412.23'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) } ]; const portfolioCalculator = new PortfolioCalculator( @@ -421,6 +632,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2148.5'), currency: Currency.USD, firstBuyDate: '2017-01-03', + fee: new Big(0), transactionCount: 1 } ] @@ -434,6 +646,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1999.9999999999998659756'), currency: Currency.USD, firstBuyDate: '2017-07-01', + fee: new Big(0), transactionCount: 1 }, { @@ -442,6 +655,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2148.5'), currency: Currency.USD, firstBuyDate: '2017-01-03', + fee: new Big(0), transactionCount: 1 } ] @@ -455,6 +669,7 @@ describe('PortfolioCalculator', () => { investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2018-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -463,6 +678,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1999.9999999999998659756'), currency: Currency.USD, firstBuyDate: '2017-07-01', + fee: new Big(0), transactionCount: 1 }, { @@ -471,6 +687,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2148.5'), currency: Currency.USD, firstBuyDate: '2017-01-03', + fee: new Big(0), transactionCount: 1 } ] @@ -495,27 +712,29 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('657.62'), - grossPerformance: new Big('-61.84'), - grossPerformancePercentage: new Big('-0.08595335390431712673'), - totalInvestment: new Big('719.46'), - positions: [ - { - averagePrice: new Big('719.46'), - currency: 'USD', - firstBuyDate: '2021-01-01', - grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 - grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 - investment: new Big('719.46'), - marketPrice: 657.62, - quantity: new Big('1'), - symbol: 'TSLA', - transactionCount: 1 - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('657.62'), + grossPerformance: new Big('-61.84'), + grossPerformancePercentage: new Big('-0.08595335390431712673'), + totalInvestment: new Big('719.46'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('719.46'), + currency: 'USD', + firstBuyDate: '2021-01-01', + grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 + grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 + investment: new Big('719.46'), + marketPrice: 657.62, + quantity: new Big('1'), + symbol: 'TSLA', + transactionCount: 1 + }) + ] + }) + ); }); it('with single TSLA and buy day start', async () => { @@ -533,27 +752,29 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('657.62'), - grossPerformance: new Big('-61.84'), - grossPerformancePercentage: new Big('-0.08595335390431712673'), - totalInvestment: new Big('719.46'), - positions: [ - { - averagePrice: new Big('719.46'), - currency: 'USD', - firstBuyDate: '2021-01-01', - grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 - grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 - investment: new Big('719.46'), - marketPrice: 657.62, - quantity: new Big('1'), - symbol: 'TSLA', - transactionCount: 1 - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('657.62'), + grossPerformance: new Big('-61.84'), + grossPerformancePercentage: new Big('-0.08595335390431712673'), + totalInvestment: new Big('719.46'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('719.46'), + currency: 'USD', + firstBuyDate: '2021-01-01', + grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 + grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 + investment: new Big('719.46'), + marketPrice: 657.62, + quantity: new Big('1'), + symbol: 'TSLA', + transactionCount: 1 + }) + ] + }) + ); }); it('with single TSLA and late start', async () => { @@ -571,27 +792,29 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('657.62'), - grossPerformance: new Big('-9.04'), - grossPerformancePercentage: new Big('-0.01356013560135601356'), - totalInvestment: new Big('719.46'), - positions: [ - { - averagePrice: new Big('719.46'), - currency: 'USD', - firstBuyDate: '2021-01-01', - grossPerformance: new Big('-9.04'), // 657.62-666.66=-9.04 - grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136 - investment: new Big('719.46'), - marketPrice: 657.62, - quantity: new Big('1'), - symbol: 'TSLA', - transactionCount: 1 - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('657.62'), + grossPerformance: new Big('-9.04'), + grossPerformancePercentage: new Big('-0.01356013560135601356'), + totalInvestment: new Big('719.46'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('719.46'), + currency: 'USD', + firstBuyDate: '2021-01-01', + grossPerformance: new Big('-9.04'), // 657.62-666.66=-9.04 + grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136 + investment: new Big('719.46'), + marketPrice: 657.62, + quantity: new Big('1'), + symbol: 'TSLA', + transactionCount: 1 + }) + ] + }) + ); }); it('with VTI only', async () => { @@ -609,30 +832,32 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('4871.5'), - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big('0.08839407904876477102'), - totalInvestment: new Big('4460.95'), - positions: [ - { - averagePrice: new Big('178.438'), - currency: 'USD', - firstBuyDate: '2019-02-01', - // see next test for details about how to calculate this - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big( - '0.0883940790487647710162214425767848424215253864940558186258745429269647266073266478435285352186572448' - ), - investment: new Big('4460.95'), - marketPrice: 194.86, - quantity: new Big('25'), - symbol: 'VTI', - transactionCount: 5 - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('4871.5'), + grossPerformance: new Big('240.4'), + grossPerformancePercentage: new Big('0.08839407904876477102'), + totalInvestment: new Big('4460.95'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('178.438'), + currency: 'USD', + firstBuyDate: '2019-02-01', + // see next test for details about how to calculate this + grossPerformance: new Big('240.4'), + grossPerformancePercentage: new Big( + '0.0883940790487647710162214425767848424215253864940558186258745429269647266073266478435285352186572448' + ), + investment: new Big('4460.95'), + marketPrice: 194.86, + quantity: new Big('25'), + symbol: 'VTI', + transactionCount: 5 + }) + ] + }) + ); }); it('with buy and sell', async () => { @@ -650,41 +875,43 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('4871.5'), - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big('0.01104605615757711361'), - totalInvestment: new Big('4460.95'), - positions: [ - { - averagePrice: new Big('0'), - currency: 'USD', - firstBuyDate: '2019-09-01', - grossPerformance: new Big('0'), - grossPerformancePercentage: new Big('0'), - investment: new Big('0'), - marketPrice: 2021.99, - quantity: new Big('0'), - symbol: 'AMZN', - transactionCount: 2 - }, - { - averagePrice: new Big('178.438'), - currency: 'USD', - firstBuyDate: '2019-02-01', - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big( - '0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552' - ), - investment: new Big('4460.95'), - marketPrice: 194.86, - quantity: new Big('25'), - symbol: 'VTI', - transactionCount: 5 - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('4871.5'), + grossPerformance: new Big('240.4'), + grossPerformancePercentage: new Big('0.01104605615757711361'), + totalInvestment: new Big('4460.95'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('0'), + currency: 'USD', + firstBuyDate: '2019-09-01', + grossPerformance: new Big('0'), + grossPerformancePercentage: new Big('0'), + investment: new Big('0'), + marketPrice: 2021.99, + quantity: new Big('0'), + symbol: 'AMZN', + transactionCount: 2 + }), + expect.objectContaining({ + averagePrice: new Big('178.438'), + currency: 'USD', + firstBuyDate: '2019-02-01', + grossPerformance: new Big('240.4'), + grossPerformancePercentage: new Big( + '0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552' + ), + investment: new Big('4460.95'), + marketPrice: 194.86, + quantity: new Big('25'), + symbol: 'VTI', + transactionCount: 5 + }) + ] + }) + ); }); it('with buy, sell, buy', async () => { @@ -702,6 +929,7 @@ describe('PortfolioCalculator', () => { investment: new Big('805.9'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 } ] @@ -715,6 +943,7 @@ describe('PortfolioCalculator', () => { investment: new Big('0'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 2 } ] @@ -728,6 +957,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1013.9'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 3 } ] @@ -742,29 +972,31 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('1086.7'), - grossPerformance: new Big('207.6'), - grossPerformancePercentage: new Big('0.2516103956224511062'), - totalInvestment: new Big('1013.9'), - positions: [ - { - averagePrice: new Big('202.78'), - currency: 'USD', - firstBuyDate: '2019-09-01', - grossPerformance: new Big('207.6'), - grossPerformancePercentage: new Big( - '0.2516103956224511061954915466429950404846' - ), - investment: new Big('1013.9'), - marketPrice: 217.34, - quantity: new Big('5'), - symbol: 'VTI', - transactionCount: 3 - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('1086.7'), + grossPerformance: new Big('207.6'), + grossPerformancePercentage: new Big('0.2516103956224511062'), + totalInvestment: new Big('1013.9'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('202.78'), + currency: 'USD', + firstBuyDate: '2019-09-01', + grossPerformance: new Big('207.6'), + grossPerformancePercentage: new Big( + '0.2516103956224511061954915466429950404846' + ), + investment: new Big('1013.9'), + marketPrice: 217.34, + quantity: new Big('5'), + symbol: 'VTI', + transactionCount: 3 + }) + ] + }) + ); }); it('with performance since Jan 1st, 2020', async () => { @@ -783,6 +1015,90 @@ describe('PortfolioCalculator', () => { investment: new Big('1443.8'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), + 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(0), + 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 + // gross performance: 1883-1711 + 3897.2-3766 = 303.2 + // gross performance percentage: 1.100526008 * 1.158880728 = 1.275378381 => 27.5378381 % + + const currentPositions = await portfolioCalculator.getCurrentPositions( + 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 } ] @@ -797,6 +1113,7 @@ describe('PortfolioCalculator', () => { investment: new Big('2923.7'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(100), transactionCount: 2 } ] @@ -813,8 +1130,12 @@ describe('PortfolioCalculator', () => { // 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') @@ -826,6 +1147,8 @@ describe('PortfolioCalculator', () => { currentValue: new Big('3897.2'), grossPerformance: new Big('303.2'), grossPerformancePercentage: new Big('0.27537838148272398344'), + netPerformance: new Big('253.2'), + netPerformancePercentage: new Big('0.2566937088951485493'), totalInvestment: new Big('2923.7'), positions: [ { @@ -840,12 +1163,101 @@ describe('PortfolioCalculator', () => { grossPerformancePercentage: new Big( '0.2753783814827239834392742298083677500037' ), + netPerformance: new Big('253.2'), // gross - 50 fees + netPerformancePercentage: new Big( + '0.2566937088951485493029975263687800261527' + ), // see details above 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(100), + 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 */ @@ -864,6 +1276,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1000000'), // 1 million currency: Currency.USD, firstBuyDate: '2010-12-31', + fee: new Big(0), transactionCount: 1 } ] @@ -877,6 +1290,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1100000'), // 1,000,000 + 100,000 currency: Currency.USD, firstBuyDate: '2010-12-31', + fee: new Big(0), transactionCount: 2 } ] @@ -892,27 +1306,31 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('1192327.999656600298238721'), - grossPerformance: new Big('92327.999656600898394721'), - grossPerformancePercentage: new Big('0.09788498099999947809'), - totalInvestment: new Big('1100000'), - positions: [ - { - averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542 - firstBuyDate: '2010-12-31', - quantity: new Big('1086022.689344541'), - symbol: 'MFA', - investment: new Big('1100000'), - marketPrice: 1.097884981, - transactionCount: 2, - grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328 - grossPerformancePercentage: new Big('0.09788498099999947808927632'), // 9.79 % - currency: 'USD' - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + hasErrors: false, + currentValue: new Big('1192327.999656600298238721'), + grossPerformance: new Big('92327.999656600898394721'), + grossPerformancePercentage: new Big('0.09788498099999947809'), + totalInvestment: new Big('1100000'), + positions: [ + expect.objectContaining({ + averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542 + firstBuyDate: '2010-12-31', + quantity: new Big('1086022.689344541'), + symbol: 'MFA', + investment: new Big('1100000'), + marketPrice: 1.097884981, + transactionCount: 2, + grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328 + grossPerformancePercentage: new Big( + '0.09788498099999947808927632' + ), // 9.79 % + currency: 'USD' + }) + ] + }) + ); }); /** @@ -933,6 +1351,7 @@ describe('PortfolioCalculator', () => { investment: new Big('200'), currency: Currency.CHF, firstBuyDate: '2012-12-31', + fee: new Big(0), transactionCount: 1 }, { @@ -941,6 +1360,7 @@ describe('PortfolioCalculator', () => { investment: new Big('300'), currency: Currency.CHF, firstBuyDate: '2012-12-31', + fee: new Big(0), transactionCount: 1 } ] @@ -954,6 +1374,7 @@ describe('PortfolioCalculator', () => { investment: new Big('200'), currency: Currency.CHF, firstBuyDate: '2012-12-31', + fee: new Big(0), transactionCount: 1 }, { @@ -962,6 +1383,7 @@ describe('PortfolioCalculator', () => { investment: new Big('300'), currency: Currency.CHF, firstBuyDate: '2012-12-31', + fee: new Big(0), transactionCount: 1 } ] @@ -977,39 +1399,41 @@ describe('PortfolioCalculator', () => { ); spy.mockRestore(); - expect(currentPositions).toEqual({ - currentValue: new Big('517'), - grossPerformance: new Big('17'), // 517 - 500 - grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4% - totalInvestment: new Big('500'), - hasErrors: false, - positions: [ - { - averagePrice: new Big('1'), - firstBuyDate: '2012-12-31', - quantity: new Big('200'), - symbol: 'SPA', - investment: new Big('200'), - marketPrice: 1.025, // 205 / 200 - transactionCount: 1, - grossPerformance: new Big('5'), // 205 - 200 - grossPerformancePercentage: new Big('0.025'), - currency: 'CHF' - }, - { - averagePrice: new Big('1'), - firstBuyDate: '2012-12-31', - quantity: new Big('300'), - symbol: 'SPB', - investment: new Big('300'), - marketPrice: 1.04, // 312 / 300 - transactionCount: 1, - grossPerformance: new Big('12'), // 312 - 300 - grossPerformancePercentage: new Big('0.04'), - currency: 'CHF' - } - ] - }); + expect(currentPositions).toEqual( + expect.objectContaining({ + currentValue: new Big('517'), + grossPerformance: new Big('17'), // 517 - 500 + grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4% + totalInvestment: new Big('500'), + hasErrors: false, + positions: [ + expect.objectContaining({ + averagePrice: new Big('1'), + firstBuyDate: '2012-12-31', + quantity: new Big('200'), + symbol: 'SPA', + investment: new Big('200'), + marketPrice: 1.025, // 205 / 200 + transactionCount: 1, + grossPerformance: new Big('5'), // 205 - 200 + grossPerformancePercentage: new Big('0.025'), + currency: 'CHF' + }), + expect.objectContaining({ + averagePrice: new Big('1'), + firstBuyDate: '2012-12-31', + quantity: new Big('300'), + symbol: 'SPB', + investment: new Big('300'), + marketPrice: 1.04, // 312 / 300 + transactionCount: 1, + grossPerformance: new Big('12'), // 312 - 300 + grossPerformancePercentage: new Big('0.04'), + currency: 'CHF' + }) + ] + }) + ); }); }); @@ -1036,18 +1460,136 @@ describe('PortfolioCalculator', () => { { date: '2019-01-01', grossPerformance: new Big('0'), + netPerformance: new Big('0'), + investment: new Big('0'), + value: new Big('0') + }, + { + date: '2020-01-01', + grossPerformance: new Big('498.3'), + netPerformance: new Big('498.3'), + investment: new Big('2923.7'), + value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) + }, + { + date: '2021-01-01', + grossPerformance: new Big('349.35'), + netPerformance: new Big('349.35'), + investment: new Big('652.55'), + value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) + } + ]); + }); + + it('with yearly and fees', async () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + const transactionPoints = [ + { + date: '2019-02-01', + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + fee: new Big(50), + transactionCount: 1 + } + ] + }, + { + date: '2019-08-03', + items: [ + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + fee: new Big(100), + transactionCount: 2 + } + ] + }, + { + date: '2020-02-02', + items: [ + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + fee: new Big(150), + transactionCount: 3 + } + ] + }, + { + date: '2021-02-01', + items: [ + { + quantity: new Big('15'), + symbol: 'VTI', + investment: new Big('2684.05'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + fee: new Big(200), + transactionCount: 4 + } + ] + }, + { + date: '2021-08-01', + items: [ + { + quantity: new Big('25'), + symbol: 'VTI', + investment: new Big('4460.95'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + fee: new Big(250), + transactionCount: 5 + } + ] + } + ]; + portfolioCalculator.setTransactionPoints(transactionPoints); + const timelineSpecification: TimelineSpecification[] = [ + { + start: '2019-01-01', + accuracy: 'year' + } + ]; + const timeline: TimelinePeriod[] = + await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-06-30' + ); + + expect(timeline).toEqual([ + { + date: '2019-01-01', + grossPerformance: new Big('0'), + netPerformance: new Big('0'), investment: new Big('0'), value: new Big('0') }, { date: '2020-01-01', grossPerformance: new Big('498.3'), + netPerformance: new Big('398.3'), // 100 fees investment: new Big('2923.7'), value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) }, { date: '2021-01-01', grossPerformance: new Big('349.35'), + netPerformance: new Big('199.35'), // 150 fees investment: new Big('652.55'), value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) } @@ -1076,180 +1618,210 @@ describe('PortfolioCalculator', () => { { date: '2019-01-01', grossPerformance: new Big('0'), + netPerformance: new Big('0'), investment: new Big('0'), value: new Big('0') }, { date: '2019-02-01', grossPerformance: new Big('0'), + netPerformance: new Big('0'), investment: new Big('1443.8'), value: new Big('1443.8') // 10 * (144.38 + days=0 * 0.08) }, { date: '2019-03-01', grossPerformance: new Big('22.4'), + netPerformance: new Big('22.4'), investment: new Big('1443.8'), value: new Big('1466.2') // 10 * (144.38 + days=28 * 0.08) }, { date: '2019-04-01', grossPerformance: new Big('47.2'), + netPerformance: new Big('47.2'), investment: new Big('1443.8'), value: new Big('1491') // 10 * (144.38 + days=59 * 0.08) }, { date: '2019-05-01', grossPerformance: new Big('71.2'), + netPerformance: new Big('71.2'), investment: new Big('1443.8'), value: new Big('1515') // 10 * (144.38 + days=89 * 0.08) }, { date: '2019-06-01', grossPerformance: new Big('96'), + netPerformance: new Big('96'), investment: new Big('1443.8'), value: new Big('1539.8') // 10 * (144.38 + days=120 * 0.08) }, { date: '2019-07-01', grossPerformance: new Big('120'), + netPerformance: new Big('120'), investment: new Big('1443.8'), value: new Big('1563.8') // 10 * (144.38 + days=150 * 0.08) }, { date: '2019-08-01', grossPerformance: new Big('144.8'), + netPerformance: new Big('144.8'), investment: new Big('1443.8'), value: new Big('1588.6') // 10 * (144.38 + days=181 * 0.08) }, { date: '2019-09-01', grossPerformance: new Big('303.1'), + netPerformance: new Big('303.1'), investment: new Big('2923.7'), value: new Big('3226.8') // 20 * (144.38 + days=212 * 0.08) }, { date: '2019-10-01', grossPerformance: new Big('351.1'), + netPerformance: new Big('351.1'), investment: new Big('2923.7'), value: new Big('3274.8') // 20 * (144.38 + days=242 * 0.08) }, { date: '2019-11-01', grossPerformance: new Big('400.7'), + netPerformance: new Big('400.7'), investment: new Big('2923.7'), value: new Big('3324.4') // 20 * (144.38 + days=273 * 0.08) }, { date: '2019-12-01', grossPerformance: new Big('448.7'), + netPerformance: new Big('448.7'), investment: new Big('2923.7'), value: new Big('3372.4') // 20 * (144.38 + days=303 * 0.08) }, { date: '2020-01-01', grossPerformance: new Big('498.3'), + netPerformance: new Big('498.3'), investment: new Big('2923.7'), value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) }, { date: '2020-02-01', grossPerformance: new Big('547.9'), + netPerformance: new Big('547.9'), investment: new Big('2923.7'), value: new Big('3471.6') // 20 * (144.38 + days=365 * 0.08) }, { date: '2020-03-01', grossPerformance: new Big('226.95'), + netPerformance: new Big('226.95'), investment: new Big('652.55'), value: new Big('879.5') // 5 * (144.38 + days=394 * 0.08) }, { date: '2020-04-01', grossPerformance: new Big('239.35'), + netPerformance: new Big('239.35'), investment: new Big('652.55'), value: new Big('891.9') // 5 * (144.38 + days=425 * 0.08) }, { date: '2020-05-01', grossPerformance: new Big('251.35'), + netPerformance: new Big('251.35'), investment: new Big('652.55'), value: new Big('903.9') // 5 * (144.38 + days=455 * 0.08) }, { date: '2020-06-01', grossPerformance: new Big('263.75'), + netPerformance: new Big('263.75'), investment: new Big('652.55'), value: new Big('916.3') // 5 * (144.38 + days=486 * 0.08) }, { date: '2020-07-01', grossPerformance: new Big('275.75'), + netPerformance: new Big('275.75'), investment: new Big('652.55'), value: new Big('928.3') // 5 * (144.38 + days=516 * 0.08) }, { date: '2020-08-01', grossPerformance: new Big('288.15'), + netPerformance: new Big('288.15'), investment: new Big('652.55'), value: new Big('940.7') // 5 * (144.38 + days=547 * 0.08) }, { date: '2020-09-01', grossPerformance: new Big('300.55'), + netPerformance: new Big('300.55'), investment: new Big('652.55'), value: new Big('953.1') // 5 * (144.38 + days=578 * 0.08) }, { date: '2020-10-01', grossPerformance: new Big('312.55'), + netPerformance: new Big('312.55'), investment: new Big('652.55'), value: new Big('965.1') // 5 * (144.38 + days=608 * 0.08) }, { date: '2020-11-01', grossPerformance: new Big('324.95'), + netPerformance: new Big('324.95'), investment: new Big('652.55'), value: new Big('977.5') // 5 * (144.38 + days=639 * 0.08) }, { date: '2020-12-01', grossPerformance: new Big('336.95'), + netPerformance: new Big('336.95'), investment: new Big('652.55'), value: new Big('989.5') // 5 * (144.38 + days=669 * 0.08) }, { date: '2021-01-01', grossPerformance: new Big('349.35'), + netPerformance: new Big('349.35'), investment: new Big('652.55'), value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) }, { date: '2021-02-01', grossPerformance: new Big('358.85'), + netPerformance: new Big('358.85'), investment: new Big('2684.05'), value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08) }, { date: '2021-03-01', grossPerformance: new Big('392.45'), + netPerformance: new Big('392.45'), investment: new Big('2684.05'), value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08) }, { date: '2021-04-01', grossPerformance: new Big('429.65'), + netPerformance: new Big('429.65'), investment: new Big('2684.05'), value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08) }, { date: '2021-05-01', grossPerformance: new Big('465.65'), + netPerformance: new Big('465.65'), investment: new Big('2684.05'), value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08) }, { date: '2021-06-01', grossPerformance: new Big('502.85'), + netPerformance: new Big('502.85'), investment: new Big('2684.05'), value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) } @@ -1282,48 +1854,56 @@ describe('PortfolioCalculator', () => { { date: '2019-01-01', grossPerformance: new Big('0'), + netPerformance: new Big('0'), investment: new Big('0'), value: new Big('0') }, { date: '2020-01-01', grossPerformance: new Big('498.3'), + netPerformance: new Big('498.3'), investment: new Big('2923.7'), value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) }, { date: '2021-01-01', grossPerformance: new Big('349.35'), + netPerformance: new Big('349.35'), investment: new Big('652.55'), value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) }, { date: '2021-02-01', grossPerformance: new Big('358.85'), + netPerformance: new Big('358.85'), investment: new Big('2684.05'), value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08) }, { date: '2021-03-01', grossPerformance: new Big('392.45'), + netPerformance: new Big('392.45'), investment: new Big('2684.05'), value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08) }, { date: '2021-04-01', grossPerformance: new Big('429.65'), + netPerformance: new Big('429.65'), investment: new Big('2684.05'), value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08) }, { date: '2021-05-01', grossPerformance: new Big('465.65'), + netPerformance: new Big('465.65'), investment: new Big('2684.05'), value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08) }, { date: '2021-06-01', grossPerformance: new Big('502.85'), + netPerformance: new Big('502.85'), investment: new Big('2684.05'), value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) } @@ -1361,222 +1941,259 @@ describe('PortfolioCalculator', () => { { date: '2019-01-01', grossPerformance: new Big('0'), + netPerformance: new Big('0'), investment: new Big('0'), value: new Big('0') }, { date: '2020-01-01', grossPerformance: new Big('498.3'), + netPerformance: new Big('498.3'), investment: new Big('2923.7'), value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) }, { date: '2021-01-01', grossPerformance: new Big('349.35'), + netPerformance: new Big('349.35'), investment: new Big('652.55'), value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) }, { date: '2021-02-01', grossPerformance: new Big('358.85'), + netPerformance: new Big('358.85'), investment: new Big('2684.05'), value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08) }, { date: '2021-03-01', grossPerformance: new Big('392.45'), + netPerformance: new Big('392.45'), investment: new Big('2684.05'), value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08) }, { date: '2021-04-01', grossPerformance: new Big('429.65'), + netPerformance: new Big('429.65'), investment: new Big('2684.05'), value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08) }, { date: '2021-05-01', grossPerformance: new Big('465.65'), + netPerformance: new Big('465.65'), investment: new Big('2684.05'), value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08) }, { date: '2021-06-01', grossPerformance: new Big('502.85'), + netPerformance: new Big('502.85'), investment: new Big('2684.05'), value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) }, { date: '2021-06-02', grossPerformance: new Big('504.05'), + netPerformance: new Big('504.05'), investment: new Big('2684.05'), value: new Big('3188.1') // 15 * (144.38 + days=852 * 0.08) / +1.2 }, { date: '2021-06-03', grossPerformance: new Big('505.25'), + netPerformance: new Big('505.25'), investment: new Big('2684.05'), value: new Big('3189.3') // +1.2 }, { date: '2021-06-04', grossPerformance: new Big('506.45'), + netPerformance: new Big('506.45'), investment: new Big('2684.05'), value: new Big('3190.5') // +1.2 }, { date: '2021-06-05', grossPerformance: new Big('507.65'), + netPerformance: new Big('507.65'), investment: new Big('2684.05'), value: new Big('3191.7') // +1.2 }, { date: '2021-06-06', grossPerformance: new Big('508.85'), + netPerformance: new Big('508.85'), investment: new Big('2684.05'), value: new Big('3192.9') // +1.2 }, { date: '2021-06-07', grossPerformance: new Big('510.05'), + netPerformance: new Big('510.05'), investment: new Big('2684.05'), value: new Big('3194.1') // +1.2 }, { date: '2021-06-08', grossPerformance: new Big('511.25'), + netPerformance: new Big('511.25'), investment: new Big('2684.05'), value: new Big('3195.3') // +1.2 }, { date: '2021-06-09', grossPerformance: new Big('512.45'), + netPerformance: new Big('512.45'), investment: new Big('2684.05'), value: new Big('3196.5') // +1.2 }, { date: '2021-06-10', grossPerformance: new Big('513.65'), + netPerformance: new Big('513.65'), investment: new Big('2684.05'), value: new Big('3197.7') // +1.2 }, { date: '2021-06-11', grossPerformance: new Big('514.85'), + netPerformance: new Big('514.85'), investment: new Big('2684.05'), value: new Big('3198.9') // +1.2 }, { date: '2021-06-12', grossPerformance: new Big('516.05'), + netPerformance: new Big('516.05'), investment: new Big('2684.05'), value: new Big('3200.1') // +1.2 }, { date: '2021-06-13', grossPerformance: new Big('517.25'), + netPerformance: new Big('517.25'), investment: new Big('2684.05'), value: new Big('3201.3') // +1.2 }, { date: '2021-06-14', grossPerformance: new Big('518.45'), + netPerformance: new Big('518.45'), investment: new Big('2684.05'), value: new Big('3202.5') // +1.2 }, { date: '2021-06-15', grossPerformance: new Big('519.65'), + netPerformance: new Big('519.65'), investment: new Big('2684.05'), value: new Big('3203.7') // +1.2 }, { date: '2021-06-16', grossPerformance: new Big('520.85'), + netPerformance: new Big('520.85'), investment: new Big('2684.05'), value: new Big('3204.9') // +1.2 }, { date: '2021-06-17', grossPerformance: new Big('522.05'), + netPerformance: new Big('522.05'), investment: new Big('2684.05'), value: new Big('3206.1') // +1.2 }, { date: '2021-06-18', grossPerformance: new Big('523.25'), + netPerformance: new Big('523.25'), investment: new Big('2684.05'), value: new Big('3207.3') // +1.2 }, { date: '2021-06-19', grossPerformance: new Big('524.45'), + netPerformance: new Big('524.45'), investment: new Big('2684.05'), value: new Big('3208.5') // +1.2 }, { date: '2021-06-20', grossPerformance: new Big('525.65'), + netPerformance: new Big('525.65'), investment: new Big('2684.05'), value: new Big('3209.7') // +1.2 }, { date: '2021-06-21', grossPerformance: new Big('526.85'), + netPerformance: new Big('526.85'), investment: new Big('2684.05'), value: new Big('3210.9') // +1.2 }, { date: '2021-06-22', grossPerformance: new Big('528.05'), + netPerformance: new Big('528.05'), investment: new Big('2684.05'), value: new Big('3212.1') // +1.2 }, { date: '2021-06-23', grossPerformance: new Big('529.25'), + netPerformance: new Big('529.25'), investment: new Big('2684.05'), value: new Big('3213.3') // +1.2 }, { date: '2021-06-24', grossPerformance: new Big('530.45'), + netPerformance: new Big('530.45'), investment: new Big('2684.05'), value: new Big('3214.5') // +1.2 }, { date: '2021-06-25', grossPerformance: new Big('531.65'), + netPerformance: new Big('531.65'), investment: new Big('2684.05'), value: new Big('3215.7') // +1.2 }, { date: '2021-06-26', grossPerformance: new Big('532.85'), + netPerformance: new Big('532.85'), investment: new Big('2684.05'), value: new Big('3216.9') // +1.2 }, { date: '2021-06-27', grossPerformance: new Big('534.05'), + netPerformance: new Big('534.05'), investment: new Big('2684.05'), value: new Big('3218.1') // +1.2 }, { date: '2021-06-28', grossPerformance: new Big('535.25'), + netPerformance: new Big('535.25'), investment: new Big('2684.05'), value: new Big('3219.3') // +1.2 }, { date: '2021-06-29', grossPerformance: new Big('536.45'), + netPerformance: new Big('536.45'), investment: new Big('2684.05'), value: new Big('3220.5') // +1.2 }, { date: '2021-06-30', grossPerformance: new Big('537.65'), + netPerformance: new Big('537.65'), investment: new Big('2684.05'), value: new Big('3221.7') // +1.2 } @@ -1599,6 +2216,7 @@ describe('PortfolioCalculator', () => { investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 1 }, { @@ -1607,6 +2225,7 @@ describe('PortfolioCalculator', () => { investment: new Big('1443.8'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 1 } ] @@ -1628,12 +2247,14 @@ describe('PortfolioCalculator', () => { { date: '2019-01-01', grossPerformance: new Big('0'), + netPerformance: new Big('0'), investment: new Big('0'), value: new Big('0') }, { date: '2020-01-01', grossPerformance: new Big('267.2'), + netPerformance: new Big('267.2'), investment: new Big('11553.75'), value: new Big('11820.95') // 10 * (144.38 + days=334 * 0.08) + 5 * 2021.99 } @@ -1650,7 +2271,8 @@ const ordersMixedSymbols: PortfolioOrder[] = [ symbol: 'TSLA', type: OrderType.Buy, unitPrice: new Big('42.97'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2017-07-01', @@ -1659,7 +2281,8 @@ const ordersMixedSymbols: PortfolioOrder[] = [ symbol: 'BTCUSD', type: OrderType.Buy, unitPrice: new Big('3562.089535970158'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2018-09-01', @@ -1668,7 +2291,8 @@ const ordersMixedSymbols: PortfolioOrder[] = [ symbol: 'AMZN', type: OrderType.Buy, unitPrice: new Big('2021.99'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) } ]; @@ -1680,7 +2304,8 @@ const ordersVTI: PortfolioOrder[] = [ symbol: 'VTI', type: OrderType.Buy, unitPrice: new Big('144.38'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2019-08-03', @@ -1689,7 +2314,8 @@ const ordersVTI: PortfolioOrder[] = [ symbol: 'VTI', type: OrderType.Buy, unitPrice: new Big('147.99'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2020-02-02', @@ -1698,7 +2324,8 @@ const ordersVTI: PortfolioOrder[] = [ symbol: 'VTI', type: OrderType.Sell, unitPrice: new Big('151.41'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2021-08-01', @@ -1707,7 +2334,8 @@ const ordersVTI: PortfolioOrder[] = [ symbol: 'VTI', type: OrderType.Buy, unitPrice: new Big('177.69'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) }, { date: '2021-02-01', @@ -1716,7 +2344,8 @@ const ordersVTI: PortfolioOrder[] = [ symbol: 'VTI', type: OrderType.Buy, unitPrice: new Big('203.15'), - currency: Currency.USD + currency: Currency.USD, + fee: new Big(0) } ]; @@ -1730,6 +2359,7 @@ const orderTslaTransactionPoint: TransactionPoint[] = [ investment: new Big('719.46'), currency: Currency.USD, firstBuyDate: '2021-01-01', + fee: new Big(0), transactionCount: 1 } ] @@ -1746,6 +2376,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [ investment: new Big('1443.8'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 1 } ] @@ -1759,6 +2390,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [ investment: new Big('2923.7'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 2 } ] @@ -1772,6 +2404,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [ investment: new Big('652.55'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 3 } ] @@ -1785,6 +2418,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [ investment: new Big('2684.05'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 4 } ] @@ -1798,6 +2432,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [ investment: new Big('4460.95'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 5 } ] @@ -1814,6 +2449,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('1443.8'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 1 } ] @@ -1827,6 +2463,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('2923.7'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 2 } ] @@ -1840,6 +2477,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -1848,6 +2486,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('2923.7'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 2 } ] @@ -1861,6 +2500,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('10109.95'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 1 }, { @@ -1869,6 +2509,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('652.55'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 3 } ] @@ -1882,6 +2523,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('0'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 2 }, { @@ -1890,6 +2532,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('652.55'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 3 } ] @@ -1903,6 +2546,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('0'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 2 }, { @@ -1911,6 +2555,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('2684.05'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 4 } ] @@ -1924,6 +2569,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('0'), currency: Currency.USD, firstBuyDate: '2019-09-01', + fee: new Big(0), transactionCount: 2 }, { @@ -1932,6 +2578,7 @@ const transactionPointsBuyAndSell = [ investment: new Big('4460.95'), currency: Currency.USD, firstBuyDate: '2019-02-01', + fee: new Big(0), transactionCount: 5 } ] diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index ba78426d6..94829e7b5 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -58,6 +58,7 @@ export class PortfolioCalculator { .plus(oldAccumulatedSymbol.quantity); currentTransactionPointItem = { currency: order.currency, + fee: order.fee.plus(oldAccumulatedSymbol.fee), firstBuyDate: oldAccumulatedSymbol.firstBuyDate, investment: newQuantity.eq(0) ? new Big(0) @@ -72,6 +73,7 @@ export class PortfolioCalculator { } else { currentTransactionPointItem = { currency: order.currency, + fee: order.fee, firstBuyDate: order.date, investment: unitPrice.mul(order.quantity).mul(factor), quantity: order.quantity.mul(factor), @@ -112,11 +114,13 @@ export class PortfolioCalculator { public async getCurrentPositions(start: Date): Promise { if (!this.transactionPoints?.length) { return { + currentValue: new Big(0), hasErrors: false, - positions: [], grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), - currentValue: new Big(0), + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + positions: [], totalInvestment: new Big(0) }; } @@ -181,7 +185,9 @@ export class PortfolioCalculator { const startString = format(start, DATE_FORMAT); const holdingPeriodReturns: { [symbol: string]: Big } = {}; + const netHoldingPeriodReturns: { [symbol: string]: Big } = {}; const grossPerformance: { [symbol: string]: Big } = {}; + const netPerformance: { [symbol: string]: Big } = {}; const todayString = format(today, DATE_FORMAT); if (firstIndex > 0) { @@ -190,6 +196,7 @@ export class PortfolioCalculator { const invalidSymbols = []; const lastInvestments: { [symbol: string]: Big } = {}; const lastQuantities: { [symbol: string]: Big } = {}; + const lastFees: { [symbol: string]: Big } = {}; const initialValues: { [symbol: string]: Big } = {}; for (let i = firstIndex; i < this.transactionPoints.length; i++) { @@ -202,10 +209,6 @@ export class PortfolioCalculator { const items = this.transactionPoints[i].items; for (const item of items) { - let oldHoldingPeriodReturn = holdingPeriodReturns[item.symbol]; - if (!oldHoldingPeriodReturn) { - oldHoldingPeriodReturn = new Big(1); - } if (!marketSymbolMap[nextDate]?.[item.symbol]) { invalidSymbols.push(item.symbol); hasErrors = true; @@ -224,6 +227,13 @@ export class PortfolioCalculator { const itemValue = marketSymbolMap[currentDate]?.[item.symbol]; let initialValue = itemValue?.mul(lastQuantity); let investedValue = itemValue?.mul(item.quantity); + const isFirstOrderAndIsStartBeforeCurrentDate = + i === firstIndex && + isBefore(parseDate(this.transactionPoints[i].date), start); + const lastFee: Big = lastFees[item.symbol] ?? new Big(0); + const fee = isFirstOrderAndIsStartBeforeCurrentDate + ? new Big(0) + : item.fee.minus(lastFee); if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) { initialValue = item.investment; investedValue = item.investment; @@ -247,18 +257,26 @@ export class PortfolioCalculator { ); const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow)); - holdingPeriodReturns[item.symbol] = - oldHoldingPeriodReturn.mul(holdingPeriodReturn); - let oldGrossPerformance = grossPerformance[item.symbol]; - if (!oldGrossPerformance) { - oldGrossPerformance = new Big(0); - } - const currentPerformance = endValue.minus(investedValue); - grossPerformance[item.symbol] = - oldGrossPerformance.plus(currentPerformance); + holdingPeriodReturns[item.symbol] = ( + holdingPeriodReturns[item.symbol] ?? new Big(1) + ).mul(holdingPeriodReturn); + grossPerformance[item.symbol] = ( + grossPerformance[item.symbol] ?? new Big(0) + ).plus(endValue.minus(investedValue)); + + const netHoldingPeriodReturn = endValue.div( + 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; lastQuantities[item.symbol] = item.quantity; + lastFees[item.symbol] = item.fee; } } @@ -282,15 +300,17 @@ export class PortfolioCalculator { : null, investment: item.investment, marketPrice: marketValue?.toNumber() ?? null, + netPerformance: isValid ? netPerformance[item.symbol] ?? null : null, + netPerformancePercentage: + isValid && netHoldingPeriodReturns[item.symbol] + ? netHoldingPeriodReturns[item.symbol].minus(1) + : null, quantity: item.quantity, symbol: item.symbol, transactionCount: item.transactionCount }); } - const overall = this.calculateOverallGrossPerformance( - positions, - initialValues - ); + const overall = this.calculateOverallPerformance(positions, initialValues); return { ...overall, @@ -378,7 +398,7 @@ export class PortfolioCalculator { return flatten(timelinePeriods); } - private calculateOverallGrossPerformance( + private calculateOverallPerformance( positions: TimelinePosition[], initialValues: { [p: string]: Big } ) { @@ -387,6 +407,8 @@ export class PortfolioCalculator { let totalInvestment = new Big(0); let grossPerformance = new Big(0); let grossPerformancePercentage = new Big(0); + let netPerformance = new Big(0); + let netPerformancePercentage = new Big(0); let completeInitialValue = new Big(0); for (const currentPosition of positions) { if (currentPosition.marketPrice) { @@ -401,6 +423,7 @@ export class PortfolioCalculator { grossPerformance = grossPerformance.plus( currentPosition.grossPerformance ); + netPerformance = netPerformance.plus(currentPosition.netPerformance); } else if (!currentPosition.quantity.eq(0)) { hasErrors = true; } @@ -414,6 +437,9 @@ export class PortfolioCalculator { grossPerformancePercentage = grossPerformancePercentage.plus( currentPosition.grossPerformancePercentage.mul(currentInitialValue) ); + netPerformancePercentage = netPerformancePercentage.plus( + currentPosition.netPerformancePercentage.mul(currentInitialValue) + ); } else if (!currentPosition.quantity.eq(0)) { console.error( `Initial value is missing for symbol ${currentPosition.symbol}` @@ -425,6 +451,8 @@ export class PortfolioCalculator { if (!completeInitialValue.eq(0)) { grossPerformancePercentage = grossPerformancePercentage.div(completeInitialValue); + netPerformancePercentage = + netPerformancePercentage.div(completeInitialValue); } return { @@ -432,6 +460,8 @@ export class PortfolioCalculator { grossPerformance, grossPerformancePercentage, hasErrors, + netPerformance, + netPerformancePercentage, totalInvestment }; } @@ -442,6 +472,7 @@ export class PortfolioCalculator { endDate: Date ): Promise { let investment: Big = new Big(0); + let fees: Big = new Big(0); const marketSymbolMap: { [date: string]: { [symbol: string]: Big }; @@ -454,6 +485,7 @@ export class PortfolioCalculator { currencies[item.symbol] = item.currency; symbols.push(item.symbol); investment = investment.add(item.investment); + fees = fees.add(item.fee); } let marketSymbols: GetValueObject[] = []; @@ -490,7 +522,7 @@ export class PortfolioCalculator { } } - const results = []; + const results: TimelinePeriod[] = []; for ( let currentDate = startDate; isBefore(currentDate, endDate); @@ -513,11 +545,13 @@ export class PortfolioCalculator { } } if (!invalid) { + const grossPerformance = value.minus(investment); const result = { - date: currentDateAsString, - grossPerformance: value.minus(investment), + grossPerformance, investment, - value + value, + date: currentDateAsString, + netPerformance: grossPerformance.minus(fees) }; results.push(result); } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fad3925a3..754e0b0e3 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -147,7 +147,7 @@ export class PortfolioService { .map((timelineItem) => ({ date: timelineItem.date, marketPrice: timelineItem.value, - value: timelineItem.grossPerformance.toNumber() + value: timelineItem.netPerformance.toNumber() })); } @@ -233,6 +233,8 @@ export class PortfolioService { marketPrice: item.marketPrice, marketState: dataProviderResponse.marketState, name: symbolProfile.name, + netPerformance: item.netPerformance?.toNumber() ?? 0, + netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0, quantity: item.quantity.toNumber(), sectors: symbolProfile.sectors, symbol: item.symbol, @@ -280,6 +282,8 @@ export class PortfolioService { marketPrice: undefined, maxPrice: undefined, minPrice: undefined, + netPerformance: undefined, + netPerformancePercent: undefined, quantity: undefined, symbol: aSymbol, transactionCount: undefined @@ -291,6 +295,7 @@ export class PortfolioService { const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ currency: order.currency, date: format(order.date, DATE_FORMAT), + fee: new Big(order.fee), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), symbol: order.symbol, @@ -324,7 +329,7 @@ export class PortfolioService { transactionCount } = position; - // Convert investment and gross performance to currency of user + // Convert investment, gross and net performance to currency of user const userCurrency = this.request.user.Settings.currency; const investment = this.exchangeRateDataService.toCurrency( position.investment.toNumber(), @@ -336,6 +341,11 @@ export class PortfolioService { currency, userCurrency ); + const netPerformance = this.exchangeRateDataService.toCurrency( + position.netPerformance.toNumber(), + currency, + userCurrency + ); const historicalData = await this.dataProviderService.getHistorical( [aSymbol], @@ -397,10 +407,12 @@ export class PortfolioService { marketPrice, maxPrice, minPrice, + netPerformance, transactionCount, averagePrice: averagePrice.toNumber(), grossPerformancePercent: position.grossPerformancePercentage.toNumber(), historicalData: historicalDataArray, + netPerformancePercent: position.netPerformancePercentage.toNumber(), quantity: quantity.toNumber(), symbol: aSymbol }; @@ -450,6 +462,8 @@ export class PortfolioService { grossPerformancePercent: undefined, historicalData: historicalDataArray, investment: 0, + netPerformance: undefined, + netPerformancePercent: undefined, quantity: 0, symbol: aSymbol, transactionCount: undefined @@ -513,6 +527,9 @@ export class PortfolioService { investment: new Big(position.investment).toNumber(), marketState: dataProviderResponses[position.symbol].marketState, name: symbolProfileMap[position.symbol].name, + netPerformance: position.netPerformance?.toNumber() ?? null, + netPerformancePercentage: + position.netPerformancePercentage?.toNumber() ?? null, quantity: new Big(position.quantity).toNumber() }; }) @@ -538,6 +555,8 @@ export class PortfolioService { performance: { currentGrossPerformance: 0, currentGrossPerformancePercent: 0, + currentNetPerformance: 0, + currentNetPerformancePercent: 0, currentValue: 0 } }; @@ -557,11 +576,17 @@ export class PortfolioService { currentPositions.grossPerformance.toNumber(); const currentGrossPerformancePercent = currentPositions.grossPerformancePercentage.toNumber(); + const currentNetPerformance = currentPositions.netPerformance.toNumber(); + const currentNetPerformancePercent = + currentPositions.netPerformancePercentage.toNumber(); + return { hasErrors: currentPositions.hasErrors || hasErrors, performance: { currentGrossPerformance, currentGrossPerformancePercent, + currentNetPerformance, + currentNetPerformancePercent, currentValue: currentValue } }; @@ -732,6 +757,8 @@ export class PortfolioService { marketPrice: 0, marketState: MarketState.open, name: 'Cash', + netPerformance: 0, + netPerformancePercent: 0, quantity: 0, sectors: [], symbol: ghostfolioCashSymbol, @@ -778,6 +805,13 @@ export class PortfolioService { const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ currency: order.currency, date: format(order.date, DATE_FORMAT), + fee: new Big( + this.exchangeRateDataService.toCurrency( + order.fee, + order.currency, + userCurrency + ) + ), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), symbol: order.symbol, diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index 4fb8c85fb..42aa5a683 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -37,7 +37,7 @@ [colorizeSign]="true" [isCurrency]="true" [locale]="locale" - [value]="isLoading ? undefined : performance?.currentGrossPerformance" + [value]="isLoading ? undefined : performance?.currentNetPerformance" >
@@ -46,7 +46,7 @@ [isPercent]="true" [locale]="locale" [value]=" - isLoading ? undefined : performance?.currentGrossPerformancePercent + isLoading ? undefined : performance?.currentNetPerformancePercent " >
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 0bb3608b5..90d134d45 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -52,7 +52,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit { new CountUp( 'value', - this.performance?.currentGrossPerformancePercent * 100, + this.performance?.currentNetPerformancePercent * 100, { decimalPlaces: 2, duration: 0.75, diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 37193396d..d2551e5fc 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -9,23 +9,6 @@

-
-
- Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 - {order} other {orders}} -
-
- -
-
-
-

-
Buy
@@ -66,7 +49,7 @@
-
Absolute Performance
+
Absolute Gross Performance
-
Performance (TWR)
+
Gross Performance (TWR)
+
+
+ Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 + {order} other {orders}} +
+
+ - + +
+
+
+

+
+
+
Absolute Net Performance
+
+ +
+
+
+
Net Performance (TWR)
+
+ +
+

diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index e9820c9eb..582cfaf6c 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -34,6 +34,8 @@ export class PositionDetailDialog implements OnDestroy { public marketPrice: number; public maxPrice: number; public minPrice: number; + public netPerformance: number; + public netPerformancePercent: number; public quantity: number; public transactionCount: number; @@ -60,6 +62,8 @@ export class PositionDetailDialog implements OnDestroy { marketPrice, maxPrice, minPrice, + netPerformance, + netPerformancePercent, quantity, transactionCount }) => { @@ -86,6 +90,8 @@ export class PositionDetailDialog implements OnDestroy { this.marketPrice = marketPrice; this.maxPrice = maxPrice; this.minPrice = minPrice; + this.netPerformance = netPerformance; + this.netPerformancePercent = netPerformancePercent; this.quantity = quantity; this.transactionCount = transactionCount; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 26e6aa43e..1847676f4 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -25,7 +25,7 @@ [colorizeSign]="true" [currency]="data.baseCurrency" [locale]="data.locale" - [value]="grossPerformance" + [value]="netPerformance" >
@@ -35,7 +35,7 @@ [colorizeSign]="true" [isPercent]="true" [locale]="data.locale" - [value]="grossPerformancePercent" + [value]="netPerformancePercent" >
diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index acda3fb9a..bb85859e3 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -11,7 +11,7 @@ [isLoading]="isLoading" [marketState]="position?.marketState" [range]="range" - [value]="position?.grossPerformancePercentage" + [value]="position?.netPerformancePercentage" >
@@ -47,13 +47,13 @@ [colorizeSign]="true" [currency]="baseCurrency" [locale]="locale" - [value]="position?.grossPerformance" + [value]="position?.netPerformance" >
diff --git a/apps/client/src/app/components/positions-table/positions-table.component.html b/apps/client/src/app/components/positions-table/positions-table.component.html index 144bfc933..7094d2672 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.html +++ b/apps/client/src/app/components/positions-table/positions-table.component.html @@ -30,7 +30,7 @@ [colorizeSign]="true" [isPercent]="true" [locale]="locale" - [value]="isLoading ? undefined : element.grossPerformancePercent" + [value]="isLoading ? undefined : element.netPerformancePercent" > diff --git a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts index 7fb176233..2051be7fd 100644 --- a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts @@ -1,5 +1,7 @@ export interface PortfolioPerformance { currentGrossPerformance: number; currentGrossPerformancePercent: number; + currentNetPerformance: number; + currentNetPerformancePercent: number; currentValue: number; } diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 3afd5c085..33d7769aa 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -20,6 +20,8 @@ export interface PortfolioPosition { marketPrice: number; marketState: MarketState; name: string; + netPerformance: number; + netPerformancePercent: number; quantity: number; sectors: Sector[]; transactionCount: number; diff --git a/libs/common/src/lib/interfaces/position.interface.ts b/libs/common/src/lib/interfaces/position.interface.ts index 694ad0cdb..830c7fef1 100644 --- a/libs/common/src/lib/interfaces/position.interface.ts +++ b/libs/common/src/lib/interfaces/position.interface.ts @@ -13,6 +13,8 @@ export interface Position { marketPrice?: number; marketState?: MarketState; name?: string; + netPerformance?: number; + netPerformancePercentage?: number; quantity: number; symbol: string; transactionCount: number; diff --git a/libs/common/src/lib/interfaces/timeline-position.interface.ts b/libs/common/src/lib/interfaces/timeline-position.interface.ts index 533c09a04..76604955a 100644 --- a/libs/common/src/lib/interfaces/timeline-position.interface.ts +++ b/libs/common/src/lib/interfaces/timeline-position.interface.ts @@ -9,6 +9,8 @@ export interface TimelinePosition { grossPerformancePercentage: Big; investment: Big; marketPrice: number; + netPerformance: Big; + netPerformancePercentage: Big; quantity: Big; symbol: string; transactionCount: number;