From 0352787ebe48ec90eebffe06848bb9e142ee70f2 Mon Sep 17 00:00:00 2001 From: Reto Kaul Date: Sat, 6 Jan 2024 18:00:18 +0100 Subject: [PATCH] Calculate currency effect --- .../portfolio/current-rate.service.mock.ts | 21 +- .../portfolio/current-rate.service.spec.ts | 21 +- .../src/app/portfolio/current-rate.service.ts | 36 +- .../interfaces/current-positions.interface.ts | 5 + .../interfaces/get-value-object.interface.ts | 2 +- .../interfaces/get-values-params.interface.ts | 2 - .../portfolio-calculator.interface.ts | 3 + .../portfolio-position-detail.interface.ts | 4 + ...folio-calculator-baln-buy-and-sell.spec.ts | 34 +- .../portfolio-calculator-baln-buy.spec.ts | 34 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 36 +- .../portfolio-calculator-googl-buy.spec.ts | 143 ++++ .../portfolio-calculator-no-orders.spec.ts | 16 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 36 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 50 +- .../portfolio/portfolio-calculator.spec.ts | 12 +- .../src/app/portfolio/portfolio-calculator.ts | 698 +++++++++++++----- .../src/app/portfolio/portfolio.service.ts | 131 ++-- .../exchange-rate-data.service.mock.ts | 19 + .../exchange-rate-data.service.ts | 61 +- .../historical-data-item.interface.ts | 4 + .../portfolio-performance.interface.ts | 4 + .../interfaces/timeline-position.interface.ts | 7 + 23 files changed, 1097 insertions(+), 282 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts create mode 100644 apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 6d64f5869..7b251551c 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -33,6 +33,15 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 0 }; + case 'GOOGL': + if (isSameDay(parseDate('2023-01-03'), date)) { + return { marketPrice: 89.12 }; + } else if (isSameDay(parseDate('2023-07-10'), date)) { + return { marketPrice: 116.45 }; + } + + return { marketPrice: 0 }; + case 'NOVN.SW': if (isSameDay(parseDate('2022-04-11'), date)) { return { marketPrice: 87.8 }; @@ -62,10 +71,8 @@ export const CurrentRateServiceMock = { values.push({ date, dataSource: dataGatheringItem.dataSource, - marketPriceInBaseCurrency: mockGetValue( - dataGatheringItem.symbol, - date - ).marketPrice, + marketPrice: mockGetValue(dataGatheringItem.symbol, date) + .marketPrice, symbol: dataGatheringItem.symbol }); } @@ -76,10 +83,8 @@ export const CurrentRateServiceMock = { values.push({ date, dataSource: dataGatheringItem.dataSource, - marketPriceInBaseCurrency: mockGetValue( - dataGatheringItem.symbol, - date - ).marketPrice, + marketPrice: mockGetValue(dataGatheringItem.symbol, date) + .marketPrice, symbol: dataGatheringItem.symbol }); } diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index 44d8bedb8..955e82b37 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -1,5 +1,4 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { UniqueAsset } from '@ghostfolio/common/interfaces'; @@ -67,7 +66,8 @@ jest.mock( initialize: () => Promise.resolve(), toCurrency: (value: number) => { return 1 * value; - } + }, + getExchangeRates: () => Promise.resolve() }; }) }; @@ -87,7 +87,6 @@ jest.mock('@ghostfolio/api/services/property/property.service', () => { describe('CurrentRateService', () => { let currentRateService: CurrentRateService; let dataProviderService: DataProviderService; - let exchangeRateDataService: ExchangeRateDataService; let marketDataService: MarketDataService; let propertyService: PropertyService; @@ -102,19 +101,11 @@ describe('CurrentRateService', () => { propertyService, null ); - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - marketDataService = new MarketDataService(null); - await exchangeRateDataService.initialize(); + marketDataService = new MarketDataService(null); currentRateService = new CurrentRateService( dataProviderService, - exchangeRateDataService, marketDataService ); }); @@ -122,13 +113,11 @@ describe('CurrentRateService', () => { it('getValues', async () => { expect( await currentRateService.getValues({ - currencies: { AMZN: 'USD' }, dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }], dateQuery: { lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)), gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)) - }, - userCurrency: 'CHF' + } }) ).toMatchObject({ dataProviderInfos: [], @@ -137,7 +126,7 @@ describe('CurrentRateService', () => { { dataSource: 'YAHOO', date: undefined, - marketPriceInBaseCurrency: 1841.823902, + marketPrice: 1841.823902, symbol: 'AMZN' } ] diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 718ec6095..75abc0acd 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -1,5 +1,4 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { resetHours } from '@ghostfolio/common/helper'; import { @@ -19,17 +18,15 @@ import { GetValuesParams } from './interfaces/get-values-params.interface'; export class CurrentRateService { public constructor( private readonly dataProviderService: DataProviderService, - private readonly exchangeRateDataService: ExchangeRateDataService, private readonly marketDataService: MarketDataService ) {} public async getValues({ - currencies, dataGatheringItems, - dateQuery, - userCurrency + dateQuery }: GetValuesParams): Promise { const dataProviderInfos: DataProviderInfo[] = []; + const includeToday = (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && @@ -45,6 +42,7 @@ export class CurrentRateService { .getQuotes({ items: dataGatheringItems }) .then((dataResultProvider) => { const result: GetValueObject[] = []; + for (const dataGatheringItem of dataGatheringItems) { if ( dataResultProvider?.[dataGatheringItem.symbol]?.dataProviderInfo @@ -58,13 +56,8 @@ export class CurrentRateService { result.push({ dataSource: dataGatheringItem.dataSource, date: today, - marketPriceInBaseCurrency: - this.exchangeRateDataService.toCurrency( - dataResultProvider?.[dataGatheringItem.symbol] - ?.marketPrice, - dataResultProvider?.[dataGatheringItem.symbol]?.currency, - userCurrency - ), + marketPrice: + dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice, symbol: dataGatheringItem.symbol }); } else { @@ -97,13 +90,8 @@ export class CurrentRateService { return { dataSource, date, - symbol, - marketPriceInBaseCurrency: - this.exchangeRateDataService.toCurrency( - marketPrice, - currencies[symbol], - userCurrency - ) + marketPrice, + symbol }; }); }) @@ -132,7 +120,7 @@ export class CurrentRateService { dataSource, symbol, date: today, - marketPriceInBaseCurrency: 0 + marketPrice: 0 }; response.values.push(value); @@ -140,10 +128,7 @@ export class CurrentRateService { const [latestValue] = response.values .filter((currentValue) => { - return ( - currentValue.symbol === symbol && - currentValue.marketPriceInBaseCurrency - ); + return currentValue.symbol === symbol && currentValue.marketPrice; }) .sort((a, b) => { if (a.date < b.date) { @@ -157,8 +142,7 @@ export class CurrentRateService { return 0; }); - value.marketPriceInBaseCurrency = - latestValue.marketPriceInBaseCurrency; + value.marketPrice = latestValue.marketPrice; } catch {} } } 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 48e6038f3..b1e6a3154 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -4,10 +4,15 @@ import Big from 'big.js'; export interface CurrentPositions extends ResponseError { positions: TimelinePosition[]; grossPerformance: Big; + grossPerformanceWithCurrencyEffect: Big; grossPerformancePercentage: Big; + grossPerformancePercentageWithCurrencyEffect: Big; netAnnualizedPerformance?: Big; + netAnnualizedPerformanceWithCurrencyEffect?: Big; netPerformance: Big; + netPerformanceWithCurrencyEffect: Big; netPerformancePercentage: Big; + netPerformancePercentageWithCurrencyEffect: Big; currentValue: Big; totalInvestment: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts b/apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts index 661170470..6c42d260c 100644 --- a/apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts @@ -2,5 +2,5 @@ import { UniqueAsset } from '@ghostfolio/common/interfaces'; export interface GetValueObject extends UniqueAsset { date: Date; - marketPriceInBaseCurrency: number; + marketPrice: number; } diff --git a/apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts b/apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts index 5b3ab7053..5cf7c8811 100644 --- a/apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts @@ -3,8 +3,6 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac import { DateQuery } from './date-query.interface'; export interface GetValuesParams { - currencies: { [symbol: string]: string }; dataGatheringItems: IDataGatheringItem[]; dateQuery: DateQuery; - userCurrency: string; } diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts index 88026cdc7..e3867f700 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts @@ -1,5 +1,8 @@ +import Big from 'big.js'; import { PortfolioOrder } from './portfolio-order.interface'; export interface PortfolioOrderItem extends PortfolioOrder { itemType?: '' | 'start' | 'end'; + unitPriceInBaseCurrency?: Big; + unitPriceInBaseCurrencyWithCurrencyEffect?: Big; } 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 827aa25fe..bee0606fc 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 @@ -13,7 +13,9 @@ export interface PortfolioPositionDetail { feeInBaseCurrency: number; firstBuyDate: string; grossPerformance: number; + grossPerformancePercentWithCurrencyEffect: number; grossPerformancePercent: number; + grossPerformanceWithCurrencyEffect: number; historicalData: HistoricalDataItem[]; investment: number; marketPrice: number; @@ -21,6 +23,8 @@ export interface PortfolioPositionDetail { minPrice: number; netPerformance: number; netPerformancePercent: number; + netPerformancePercentWithCurrencyEffect: number; + netPerformanceWithCurrencyEffect: number; orders: OrderWithAccount[]; quantity: number; SymbolProfile: EnhancedSymbolProfile; diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts index f4fec026a..afe489614 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -4,6 +4,7 @@ import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -16,15 +17,24 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('get current positions', () => { it.only('with BALN.SW buy and sell', async () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'CHF', orders: [ { @@ -74,9 +84,17 @@ describe('PortfolioCalculator', () => { errors: [], grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.0440867739678096571' + ), + grossPerformanceWithCurrencyEffect: new Big('-12.6'), hasErrors: false, netPerformance: new Big('-15.8'), netPerformancePercentage: new Big('-0.0552834149755073478'), + netPerformancePercentageWithCurrencyEffect: new Big( + '-0.0552834149755073478' + ), + netPerformanceWithCurrencyEffect: new Big('-15.8'), positions: [ { averagePrice: new Big('0'), @@ -86,17 +104,29 @@ describe('PortfolioCalculator', () => { firstBuyDate: '2021-11-22', grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.0440867739678096571' + ), + grossPerformanceWithCurrencyEffect: new Big('-12.6'), investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), netPerformance: new Big('-15.8'), netPerformancePercentage: new Big('-0.0552834149755073478'), + netPerformancePercentageWithCurrencyEffect: new Big( + '-0.0552834149755073478' + ), + netPerformanceWithCurrencyEffect: new Big('-15.8'), marketPrice: 148.9, + marketPriceInBaseCurrency: 148.9, quantity: new Big('0'), symbol: 'BALN.SW', timeWeightedInvestment: new Big('285.8'), + timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'), transactionCount: 2 } ], - totalInvestment: new Big('0') + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts index e2560cfbb..132406922 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts @@ -4,6 +4,7 @@ import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -16,15 +17,24 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('get current positions', () => { it.only('with BALN.SW buy', async () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'CHF', orders: [ { @@ -63,9 +73,17 @@ describe('PortfolioCalculator', () => { errors: [], grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.09004392386530014641' + ), + grossPerformanceWithCurrencyEffect: new Big('24.6'), hasErrors: false, netPerformance: new Big('23.05'), netPerformancePercentage: new Big('0.08437042459736456808'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.08437042459736456808' + ), + netPerformanceWithCurrencyEffect: new Big('23.05'), positions: [ { averagePrice: new Big('136.6'), @@ -75,17 +93,29 @@ describe('PortfolioCalculator', () => { firstBuyDate: '2021-11-30', grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.09004392386530014641' + ), + grossPerformanceWithCurrencyEffect: new Big('24.6'), investment: new Big('273.2'), + investmentWithCurrencyEffect: new Big('273.2'), netPerformance: new Big('23.05'), netPerformancePercentage: new Big('0.08437042459736456808'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.08437042459736456808' + ), + netPerformanceWithCurrencyEffect: new Big('23.05'), marketPrice: 148.9, + marketPriceInBaseCurrency: 148.9, quantity: new Big('2'), symbol: 'BALN.SW', timeWeightedInvestment: new Big('273.2'), + timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'), transactionCount: 1 } ], - totalInvestment: new Big('273.2') + totalInvestment: new Big('273.2'), + totalInvestmentWithCurrencyEffect: new Big('273.2') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 4f1bc8cdc..64515a4b5 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -4,6 +4,7 @@ import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -16,15 +17,24 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('get current positions', () => { it.only('with BTCUSD buy and sell partially', async () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'CHF', orders: [ { @@ -74,9 +84,17 @@ describe('PortfolioCalculator', () => { errors: [], grossPerformance: new Big('27172.74'), grossPerformancePercentage: new Big('42.41978276196153750666'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '42.41978276196153750666' + ), + grossPerformanceWithCurrencyEffect: new Big('27172.74'), hasErrors: false, netPerformance: new Big('27172.74'), netPerformancePercentage: new Big('42.41978276196153750666'), + netPerformancePercentageWithCurrencyEffect: new Big( + '42.41978276196153750666' + ), + netPerformanceWithCurrencyEffect: new Big('27172.74'), positions: [ { averagePrice: new Big('320.43'), @@ -86,17 +104,31 @@ describe('PortfolioCalculator', () => { firstBuyDate: '2015-01-01', grossPerformance: new Big('27172.74'), grossPerformancePercentage: new Big('42.41978276196153750666'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '42.41978276196153750666' + ), + grossPerformanceWithCurrencyEffect: new Big('27172.74'), investment: new Big('320.43'), + investmentWithCurrencyEffect: new Big('320.43'), netPerformance: new Big('27172.74'), netPerformancePercentage: new Big('42.41978276196153750666'), + netPerformancePercentageWithCurrencyEffect: new Big( + '42.41978276196153750666' + ), + netPerformanceWithCurrencyEffect: new Big('27172.74'), marketPrice: 13657.2, + marketPriceInBaseCurrency: 13657.2, quantity: new Big('1'), symbol: 'BTCUSD', timeWeightedInvestment: new Big('640.56763686131386861314'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '640.56763686131386861314' + ), transactionCount: 2 } ], - totalInvestment: new Big('320.43') + totalInvestment: new Big('320.43'), + totalInvestmentWithCurrencyEffect: new Big('320.43') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts new file mode 100644 index 000000000..ca8e2339f --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts @@ -0,0 +1,143 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { parseDate } from '@ghostfolio/common/helper'; +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', + () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + ExchangeRateDataService: jest.fn().mockImplementation(() => { + return ExchangeRateDataServiceMock; + }) + }; + } +); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + }); + + describe('get current positions', () => { + it.only('with GOOGL buy', async () => { + const portfolioCalculator = new PortfolioCalculator({ + currentRateService, + exchangeRateDataService, + currency: 'CHF', + orders: [ + { + currency: 'USD', + date: '2023-01-03', + dataSource: 'YAHOO', + fee: new Big(1), + name: 'Alphabet Inc.', + quantity: new Big(1), + symbol: 'GOOGL', + type: 'BUY', + unitPrice: new Big(89.12) + } + ] + }); + + portfolioCalculator.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2023-07-10').getTime()); + + const currentPositions = await portfolioCalculator.getCurrentPositions( + parseDate('2023-01-03') + ); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = + portfolioCalculator.getInvestmentsByGroup('month'); + + spy.mockRestore(); + + expect(currentPositions).toEqual({ + currentValue: new Big('103.512405'), + errors: [], + grossPerformance: new Big('27.33'), + grossPerformancePercentage: new Big('0.3066651705565529623'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.25859529729394801776' + ), + grossPerformanceWithCurrencyEffect: new Big('21.268013'), + hasErrors: false, + netPerformance: new Big('26.33'), + netPerformancePercentage: new Big('0.29544434470377019749'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.24737447144116525295' + ), + netPerformanceWithCurrencyEffect: new Big('20.345163'), + positions: [ + { + averagePrice: new Big('89.12'), + currency: 'USD', + dataSource: 'YAHOO', + fee: new Big('1'), + firstBuyDate: '2023-01-03', + grossPerformance: new Big('27.33'), + grossPerformancePercentage: new Big('0.3066651705565529623'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.25859529729394801776' + ), + grossPerformanceWithCurrencyEffect: new Big('21.268013'), + investment: new Big('89.12'), + investmentWithCurrencyEffect: new Big('82.244392'), + netPerformance: new Big('26.33'), + netPerformancePercentage: new Big('0.29544434470377019749'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.24737447144116525295' + ), + netPerformanceWithCurrencyEffect: new Big('20.345163'), + marketPrice: 116.45, + marketPriceInBaseCurrency: 103.512405, + quantity: new Big('1'), + symbol: 'GOOGL', + timeWeightedInvestment: new Big('89.12'), + timeWeightedInvestmentWithCurrencyEffect: new Big('82.244392'), + transactionCount: 1 + } + ], + totalInvestment: new Big('89.12'), + totalInvestmentWithCurrencyEffect: new Big('82.244392') + }); + + expect(investments).toEqual([ + { date: '2023-01-03', investment: new Big('89.12') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2023-01-01', investment: new Big('89.12') } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts index a26426017..3a3b62a69 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts @@ -4,6 +4,7 @@ import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -16,15 +17,24 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('get current positions', () => { it('with no orders', async () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'CHF', orders: [] }); @@ -50,9 +60,13 @@ describe('PortfolioCalculator', () => { currentValue: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), + grossPerformancePercentageWithCurrencyEffect: new Big(0), + grossPerformanceWithCurrencyEffect: new Big(0), hasErrors: false, netPerformance: new Big(0), netPerformancePercentage: new Big(0), + netPerformancePercentageWithCurrencyEffect: new Big(0), + netPerformanceWithCurrencyEffect: new Big(0), positions: [], totalInvestment: new Big(0) }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 2c8bff238..a2c1f01b9 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -4,6 +4,7 @@ import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -16,15 +17,24 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('get current positions', () => { it.only('with NOVN.SW buy and sell partially', async () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'CHF', orders: [ { @@ -74,9 +84,17 @@ describe('PortfolioCalculator', () => { errors: [], grossPerformance: new Big('21.93'), grossPerformancePercentage: new Big('0.15113417083448194384'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.15113417083448194384' + ), + grossPerformanceWithCurrencyEffect: new Big('21.93'), hasErrors: false, netPerformance: new Big('17.68'), netPerformancePercentage: new Big('0.12184460284330327256'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.12184460284330327256' + ), + netPerformanceWithCurrencyEffect: new Big('17.68'), positions: [ { averagePrice: new Big('75.80'), @@ -85,18 +103,32 @@ describe('PortfolioCalculator', () => { fee: new Big('4.25'), firstBuyDate: '2022-03-07', grossPerformance: new Big('21.93'), + grossPerformanceWithCurrencyEffect: new Big('21.93'), grossPerformancePercentage: new Big('0.15113417083448194384'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.15113417083448194384' + ), investment: new Big('75.80'), + investmentWithCurrencyEffect: new Big('75.80'), netPerformance: new Big('17.68'), netPerformancePercentage: new Big('0.12184460284330327256'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.12184460284330327256' + ), + netPerformanceWithCurrencyEffect: new Big('17.68'), marketPrice: 87.8, + marketPriceInBaseCurrency: 87.8, quantity: new Big('1'), symbol: 'NOVN.SW', timeWeightedInvestment: new Big('145.10285714285714285714'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '145.10285714285714285714' + ), transactionCount: 2 } ], - totalInvestment: new Big('75.80') + totalInvestment: new Big('75.80'), + totalInvestmentWithCurrencyEffect: new Big('75.80') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts index 3b34a0e34..4eed52633 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -4,6 +4,7 @@ import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -16,15 +17,24 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('get current positions', () => { it.only('with NOVN.SW buy and sell', async () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'CHF', orders: [ { @@ -75,18 +85,26 @@ describe('PortfolioCalculator', () => { expect(chartData[0]).toEqual({ date: '2022-03-07', - netPerformanceInPercentage: 0, netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, totalInvestment: 151.6, - value: 151.6 + totalInvestmentValueWithCurrencyEffect: 151.6, + value: 151.6, + valueWithCurrencyEffect: 151.6 }); expect(chartData[chartData.length - 1]).toEqual({ date: '2022-04-11', - netPerformanceInPercentage: 13.100263852242744, netPerformance: 19.86, + netPerformanceInPercentage: 13.100263852242744, + netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, + netPerformanceWithCurrencyEffect: 19.86, totalInvestment: 0, - value: 0 + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 }); expect(currentPositions).toEqual({ @@ -94,9 +112,17 @@ describe('PortfolioCalculator', () => { errors: [], grossPerformance: new Big('19.86'), grossPerformancePercentage: new Big('0.13100263852242744063'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + grossPerformanceWithCurrencyEffect: new Big('19.86'), hasErrors: false, netPerformance: new Big('19.86'), netPerformancePercentage: new Big('0.13100263852242744063'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + netPerformanceWithCurrencyEffect: new Big('19.86'), positions: [ { averagePrice: new Big('0'), @@ -106,17 +132,29 @@ describe('PortfolioCalculator', () => { firstBuyDate: '2022-03-07', grossPerformance: new Big('19.86'), grossPerformancePercentage: new Big('0.13100263852242744063'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + grossPerformanceWithCurrencyEffect: new Big('19.86'), investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), netPerformance: new Big('19.86'), netPerformancePercentage: new Big('0.13100263852242744063'), + netPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + netPerformanceWithCurrencyEffect: new Big('19.86'), marketPrice: 87.8, + marketPriceInBaseCurrency: 87.8, quantity: new Big('0'), symbol: 'NOVN.SW', timeWeightedInvestment: new Big('151.6'), + timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), transactionCount: 2 } ], - totalInvestment: new Big('0') + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 23f0a8a8d..fe2ebe121 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -2,17 +2,27 @@ import Big from 'big.js'; import { CurrentRateService } from './current-rate.service'; import { PortfolioCalculator } from './portfolio-calculator'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); }); describe('annualized performance percentage', () => { const portfolioCalculator = new PortfolioCalculator({ currentRateService, + exchangeRateDataService, currency: 'USD', orders: [] }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 71ddbb6dd..cddb8bb51 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -28,7 +28,15 @@ import { set, subDays } from 'date-fns'; -import { first, flatten, isNumber, last, sortBy, uniq } from 'lodash'; +import { + cloneDeep, + first, + flatten, + isNumber, + last, + sortBy, + uniq +} from 'lodash'; import { CurrentRateService } from './current-rate.service'; import { CurrentPositions } from './interfaces/current-positions.interface'; @@ -42,30 +50,32 @@ import { } from './interfaces/timeline-specification.interface'; import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; export class PortfolioCalculator { - private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT = - true; - - private static readonly ENABLE_LOGGING = false; + private static ENABLE_LOGGING = false; private currency: string; private currentRateService: CurrentRateService; private dataProviderInfos: DataProviderInfo[]; + private exchangeRateDataService: ExchangeRateDataService; private orders: PortfolioOrder[]; private transactionPoints: TransactionPoint[]; public constructor({ currency, currentRateService, + exchangeRateDataService, orders }: { currency: string; currentRateService: CurrentRateService; + exchangeRateDataService: ExchangeRateDataService; orders: PortfolioOrder[]; }) { this.currency = currency; this.currentRateService = currentRateService; + this.exchangeRateDataService = exchangeRateDataService; this.orders = orders; this.orders.sort((a, b) => a.date?.localeCompare(b.date)); @@ -213,12 +223,10 @@ export class PortfolioCalculator { const { dataProviderInfos, values: marketSymbols } = await this.currentRateService.getValues({ - currencies, dataGatheringItems, dateQuery: { in: dates - }, - userCurrency: this.currency + } }); this.dataProviderInfos = dataProviderInfos; @@ -227,60 +235,81 @@ export class PortfolioCalculator { [date: string]: { [symbol: string]: Big }; } = {}; + let exchangeRatesByCurrency = + await this.exchangeRateDataService.getExchangeRatesByCurrency({ + currencies: uniq(Object.values(currencies)), + endDate: endOfDay(end), + startDate: parseDate(this.transactionPoints?.[0]?.date), + targetCurrency: this.currency + }); + for (const marketSymbol of marketSymbols) { const dateString = format(marketSymbol.date, DATE_FORMAT); if (!marketSymbolMap[dateString]) { marketSymbolMap[dateString] = {}; } - if (marketSymbol.marketPriceInBaseCurrency) { + if (marketSymbol.marketPrice) { marketSymbolMap[dateString][marketSymbol.symbol] = new Big( - marketSymbol.marketPriceInBaseCurrency + marketSymbol.marketPrice ); } } const accumulatedValuesByDate: { [date: string]: { - maxTotalInvestmentValue: Big; totalCurrentValue: Big; + totalCurrentValueWithCurrencyEffect: Big; totalInvestmentValue: Big; + totalInvestmentValueWithCurrencyEffect: Big; totalNetPerformanceValue: Big; + totalNetPerformanceValueWithCurrencyEffect: Big; totalTimeWeightedInvestmentValue: Big; + totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; }; } = {}; const valuesBySymbol: { [symbol: string]: { currentValues: { [date: string]: Big }; + currentValuesWithCurrencyEffect: { [date: string]: Big }; investmentValues: { [date: string]: Big }; - maxInvestmentValues: { [date: string]: Big }; + investmentValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValues: { [date: string]: Big }; + netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; timeWeightedInvestmentValues: { [date: string]: Big }; + timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; }; } = {}; for (const symbol of Object.keys(symbols)) { const { currentValues, + currentValuesWithCurrencyEffect, investmentValues, - maxInvestmentValues, + investmentValuesWithCurrencyEffect, netPerformanceValues, - timeWeightedInvestmentValues + netPerformanceValuesWithCurrencyEffect, + timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect } = this.getSymbolMetrics({ end, marketSymbolMap, start, step, symbol, + exchangeRates: exchangeRatesByCurrency[currencies[symbol]], isChartMode: true }); valuesBySymbol[symbol] = { currentValues, + currentValuesWithCurrencyEffect, investmentValues, - maxInvestmentValues, + investmentValuesWithCurrencyEffect, netPerformanceValues, - timeWeightedInvestmentValues + netPerformanceValuesWithCurrencyEffect, + timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect }; } @@ -292,63 +321,108 @@ export class PortfolioCalculator { const currentValue = symbolValues.currentValues?.[dateString] ?? new Big(0); + + const currentValueWithCurrencyEffect = + symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? + new Big(0); + const investmentValue = symbolValues.investmentValues?.[dateString] ?? new Big(0); - const maxInvestmentValue = - symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0); + + const investmentValueWithCurrencyEffect = + symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? + new Big(0); + const netPerformanceValue = symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); + + const netPerformanceValueWithCurrencyEffect = + symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ?? + new Big(0); + const timeWeightedInvestmentValue = symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0); + const timeWeightedInvestmentValueWithCurrencyEffect = + symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[ + dateString + ] ?? new Big(0); + accumulatedValuesByDate[dateString] = { totalCurrentValue: ( accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) ).add(currentValue), + totalCurrentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalCurrentValueWithCurrencyEffect ?? new Big(0) + ).add(currentValueWithCurrencyEffect), totalInvestmentValue: ( accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? new Big(0) ).add(investmentValue), + totalInvestmentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) + ).add(investmentValueWithCurrencyEffect), totalTimeWeightedInvestmentValue: ( accumulatedValuesByDate[dateString] ?.totalTimeWeightedInvestmentValue ?? new Big(0) ).add(timeWeightedInvestmentValue), - maxTotalInvestmentValue: ( - accumulatedValuesByDate[dateString]?.maxTotalInvestmentValue ?? - new Big(0) - ).add(maxInvestmentValue), + totalTimeWeightedInvestmentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0) + ).add(timeWeightedInvestmentValueWithCurrencyEffect), totalNetPerformanceValue: ( accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0) - ).add(netPerformanceValue) + ).add(netPerformanceValue), + totalNetPerformanceValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0) + ).add(netPerformanceValueWithCurrencyEffect) }; } } return Object.entries(accumulatedValuesByDate).map(([date, values]) => { const { - maxTotalInvestmentValue, totalCurrentValue, + totalCurrentValueWithCurrencyEffect, totalInvestmentValue, + totalInvestmentValueWithCurrencyEffect, totalNetPerformanceValue, - totalTimeWeightedInvestmentValue + totalNetPerformanceValueWithCurrencyEffect, + totalTimeWeightedInvestmentValue, + totalTimeWeightedInvestmentValueWithCurrencyEffect } = values; - let investmentValue = - PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT - ? totalTimeWeightedInvestmentValue - : maxTotalInvestmentValue; - - const netPerformanceInPercentage = investmentValue.eq(0) + const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0) ? 0 - : totalNetPerformanceValue.div(investmentValue).mul(100).toNumber(); + : totalNetPerformanceValue + .div(totalTimeWeightedInvestmentValue) + .mul(100) + .toNumber(); + + const netPerformanceInPercentageWithCurrencyEffect = + totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0) + ? 0 + : totalNetPerformanceValueWithCurrencyEffect + .div(totalTimeWeightedInvestmentValueWithCurrencyEffect) + .mul(100) + .toNumber(); return { date, netPerformanceInPercentage, + netPerformanceInPercentageWithCurrencyEffect, netPerformance: totalNetPerformanceValue.toNumber(), + netPerformanceWithCurrencyEffect: + totalNetPerformanceValueWithCurrencyEffect.toNumber(), totalInvestment: totalInvestmentValue.toNumber(), - value: totalCurrentValue.toNumber() + totalInvestmentValueWithCurrencyEffect: + totalInvestmentValueWithCurrencyEffect.toNumber(), + value: totalCurrentValue.toNumber(), + valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber() }; }); } @@ -366,10 +440,14 @@ export class PortfolioCalculator { return { currentValue: new Big(0), grossPerformance: new Big(0), + grossPerformanceWithCurrencyEffect: new Big(0), grossPerformancePercentage: new Big(0), + grossPerformancePercentageWithCurrencyEffect: new Big(0), hasErrors: false, netPerformance: new Big(0), + netPerformanceWithCurrencyEffect: new Big(0), netPerformancePercentage: new Big(0), + netPerformancePercentageWithCurrencyEffect: new Big(0), positions: [], totalInvestment: new Big(0) }; @@ -378,11 +456,11 @@ export class PortfolioCalculator { const lastTransactionPoint = transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1]; - let firstTransactionPoint: TransactionPoint = null; - let firstIndex = transactionPointsBeforeEndDate.length; - let dates = []; - const dataGatheringItems: IDataGatheringItem[] = []; const currencies: { [symbol: string]: string } = {}; + const dataGatheringItems: IDataGatheringItem[] = []; + let dates: Date[] = []; + let firstIndex = transactionPointsBeforeEndDate.length; + let firstTransactionPoint: TransactionPoint = null; dates.push(resetHours(start)); for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { @@ -390,8 +468,10 @@ export class PortfolioCalculator { dataSource: item.dataSource, symbol: item.symbol }); + currencies[item.symbol] = item.currency; } + for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) { if ( !isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) && @@ -428,17 +508,23 @@ export class PortfolioCalculator { }); dates.sort((a, b) => a.getTime() - b.getTime()); + let exchangeRatesByCurrency = + await this.exchangeRateDataService.getExchangeRatesByCurrency({ + currencies: uniq(Object.values(currencies)), + endDate: endOfDay(end), + startDate: parseDate(this.transactionPoints?.[0]?.date), + targetCurrency: this.currency + }); + const { dataProviderInfos, errors: currentRateErrors, values: marketSymbols } = await this.currentRateService.getValues({ - currencies, dataGatheringItems, dateQuery: { in: dates - }, - userCurrency: this.currency + } }); this.dataProviderInfos = dataProviderInfos; @@ -449,12 +535,14 @@ export class PortfolioCalculator { for (const marketSymbol of marketSymbols) { const date = format(marketSymbol.date, DATE_FORMAT); + if (!marketSymbolMap[date]) { marketSymbolMap[date] = {}; } - if (marketSymbol.marketPriceInBaseCurrency) { + + if (marketSymbol.marketPrice) { marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPriceInBaseCurrency + marketSymbol.marketPrice ); } } @@ -471,19 +559,29 @@ export class PortfolioCalculator { const errors: ResponseError['errors'] = []; for (const item of lastTransactionPoint.items) { - const marketValue = marketSymbolMap[endDateString]?.[item.symbol]; + const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[ + item.symbol + ]?.mul(exchangeRatesByCurrency[item.currency]?.[endDateString]); const { grossPerformance, + grossPerformanceWithCurrencyEffect, grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, hasErrors, netPerformance, + netPerformanceWithCurrencyEffect, netPerformancePercentage, - timeWeightedInvestment + netPerformancePercentageWithCurrencyEffect, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, + totalInvestment, + totalInvestmentWithCurrencyEffect } = this.getSymbolMetrics({ end, marketSymbolMap, start, + exchangeRates: exchangeRatesByCurrency[item.currency], symbol: item.symbol }); @@ -491,6 +589,7 @@ export class PortfolioCalculator { positions.push({ timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, averagePrice: item.quantity.eq(0) ? new Big(0) : item.investment.div(item.quantity), @@ -499,15 +598,31 @@ export class PortfolioCalculator { fee: item.fee, firstBuyDate: item.firstBuyDate, grossPerformance: !hasErrors ? grossPerformance ?? null : null, + grossPerformanceWithCurrencyEffect: !hasErrors + ? grossPerformanceWithCurrencyEffect ?? null + : null, grossPerformancePercentage: !hasErrors ? grossPerformancePercentage ?? null : null, - investment: item.investment, - marketPrice: marketValue?.toNumber() ?? null, + grossPerformancePercentageWithCurrencyEffect: !hasErrors + ? grossPerformancePercentageWithCurrencyEffect ?? null + : null, + investment: totalInvestment, + investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, + marketPrice: + marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, + marketPriceInBaseCurrency: + marketPriceInBaseCurrency?.toNumber() ?? null, netPerformance: !hasErrors ? netPerformance ?? null : null, + netPerformanceWithCurrencyEffect: !hasErrors + ? netPerformanceWithCurrencyEffect ?? null + : null, netPerformancePercentage: !hasErrors ? netPerformancePercentage ?? null : null, + netPerformancePercentageWithCurrencyEffect: !hasErrors + ? netPerformancePercentageWithCurrencyEffect ?? null + : null, quantity: item.quantity, symbol: item.symbol, tags: item.tags, @@ -751,28 +866,56 @@ export class PortfolioCalculator { private calculateOverallPerformance(positions: TimelinePosition[]) { let currentValue = new Big(0); let grossPerformance = new Big(0); + let grossPerformanceWithCurrencyEffect = new Big(0); let hasErrors = false; let netPerformance = new Big(0); + let netPerformanceWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); + let totalInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestment = new Big(0); + let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); for (const currentPosition of positions) { - if (currentPosition.marketPrice) { + if ( + currentPosition.marketPriceInBaseCurrency && + currentPosition.investment + ) { currentValue = currentValue.plus( - new Big(currentPosition.marketPrice).mul(currentPosition.quantity) + new Big(currentPosition.marketPriceInBaseCurrency).mul( + currentPosition.quantity + ) ); } else { hasErrors = true; } - totalInvestment = totalInvestment.plus(currentPosition.investment); + if (currentPosition.investment) { + totalInvestment = totalInvestment.plus(currentPosition.investment); + + totalInvestmentWithCurrencyEffect = + totalInvestmentWithCurrencyEffect.plus( + currentPosition.investmentWithCurrencyEffect + ); + } else { + hasErrors = true; + } if (currentPosition.grossPerformance) { grossPerformance = grossPerformance.plus( currentPosition.grossPerformance ); + grossPerformanceWithCurrencyEffect = + grossPerformanceWithCurrencyEffect.plus( + currentPosition.grossPerformanceWithCurrencyEffect + ); + netPerformance = netPerformance.plus(currentPosition.netPerformance); + + netPerformanceWithCurrencyEffect = + netPerformanceWithCurrencyEffect.plus( + currentPosition.netPerformanceWithCurrencyEffect + ); } else if (!currentPosition.quantity.eq(0)) { hasErrors = true; } @@ -781,11 +924,17 @@ export class PortfolioCalculator { totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus( currentPosition.timeWeightedInvestment ); + + totalTimeWeightedInvestmentWithCurrencyEffect = + totalTimeWeightedInvestmentWithCurrencyEffect.plus( + currentPosition.timeWeightedInvestmentWithCurrencyEffect + ); } else if (!currentPosition.quantity.eq(0)) { Logger.warn( `Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, 'PortfolioCalculator' ); + hasErrors = true; } } @@ -793,15 +942,30 @@ export class PortfolioCalculator { return { currentValue, grossPerformance, + grossPerformanceWithCurrencyEffect, hasErrors, netPerformance, + netPerformanceWithCurrencyEffect, totalInvestment, + totalInvestmentWithCurrencyEffect, netPerformancePercentage: totalTimeWeightedInvestment.eq(0) ? new Big(0) : netPerformance.div(totalTimeWeightedInvestment), + netPerformancePercentageWithCurrencyEffect: + totalTimeWeightedInvestmentWithCurrencyEffect.eq(0) + ? new Big(0) + : netPerformanceWithCurrencyEffect.div( + totalTimeWeightedInvestmentWithCurrencyEffect + ), grossPerformancePercentage: totalTimeWeightedInvestment.eq(0) ? new Big(0) - : grossPerformance.div(totalTimeWeightedInvestment) + : grossPerformance.div(totalTimeWeightedInvestment), + grossPerformancePercentageWithCurrencyEffect: + totalTimeWeightedInvestmentWithCurrencyEffect.eq(0) + ? new Big(0) + : grossPerformanceWithCurrencyEffect.div( + totalTimeWeightedInvestmentWithCurrencyEffect + ) }; } @@ -834,13 +998,11 @@ export class PortfolioCalculator { if (dataGatheringItems.length > 0) { try { const { values } = await this.currentRateService.getValues({ - currencies, dataGatheringItems, dateQuery: { gte: startDate, lt: endOfDay(endDate) - }, - userCurrency: this.currency + } }); marketSymbols = values; } catch (error) { @@ -858,9 +1020,9 @@ export class PortfolioCalculator { if (!marketSymbolMap[date]) { marketSymbolMap[date] = {}; } - if (marketSymbol.marketPriceInBaseCurrency) { + if (marketSymbol.marketPrice) { marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPriceInBaseCurrency + marketSymbol.marketPrice ); } } @@ -955,6 +1117,7 @@ export class PortfolioCalculator { private getSymbolMetrics({ end, + exchangeRates, isChartMode = false, marketSymbolMap, start, @@ -962,6 +1125,7 @@ export class PortfolioCalculator { symbol }: { end: Date; + exchangeRates: { [dateString: string]: number }; isChartMode?: boolean; marketSymbolMap: { [date: string]: { [symbol: string]: Big }; @@ -970,21 +1134,68 @@ export class PortfolioCalculator { step?: number; symbol: string; }) { - let orders: PortfolioOrderItem[] = this.orders.filter((order) => { - return order.symbol === symbol; - }); + const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; + const currentValues: { [date: string]: Big } = {}; + const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; + let fees = new Big(0); + let feesAtStartDate = new Big(0); + let feesAtStartDateWithCurrencyEffect = new Big(0); + let feesWithCurrencyEffect = new Big(0); + let grossPerformance = new Big(0); + let grossPerformanceWithCurrencyEffect = new Big(0); + let grossPerformanceAtStartDate = new Big(0); + let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); + let grossPerformanceFromSells = new Big(0); + let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); + let initialValue: Big; + let initialValueWithCurrencyEffect: Big; + let investmentAtStartDate: Big; + let investmentAtStartDateWithCurrencyEffect: Big; + const investmentValues: { [date: string]: Big } = {}; + const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; + let lastAveragePrice = new Big(0); + let lastAveragePriceWithCurrencyEffect = new Big(0); + const netPerformanceValues: { [date: string]: Big } = {}; + const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; + const timeWeightedInvestmentValues: { [date: string]: Big } = {}; + + const timeWeightedInvestmentValuesWithCurrencyEffect: { + [date: string]: Big; + } = {}; + + let totalInvestment = new Big(0); + let totalInvestmentWithCurrencyEffect = new Big(0); + let totalInvestmentWithGrossPerformanceFromSell = new Big(0); + + let totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect = new Big( + 0 + ); + + let totalUnits = new Big(0); + let valueAtStartDate: Big; + let valueAtStartDateWithCurrencyEffect: Big; + + // Clone orders to keep the original values in this.orders + let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter( + (order) => { + return order.symbol === symbol; + } + ); if (orders.length <= 0) { return { currentValues: {}, grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), + grossPerformanceWithCurrencyEffect: new Big(0), + grossPerformanceWithCurrencyEffectPercentage: new Big(0), hasErrors: false, initialValue: new Big(0), investmentValues: {}, - maxInvestmentValues: {}, netPerformance: new Big(0), netPerformancePercentage: new Big(0), + netPerformanceWithCurrencyEffect: new Big(0), + netPerformanceWithCurrencyEffectPercentage: new Big(0), netPerformanceValues: {} }; } @@ -1002,36 +1213,19 @@ export class PortfolioCalculator { (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) ) { return { + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0), + grossPerformanceWithCurrencyEffect: new Big(0), + grossPerformanceWithCurrencyEffectPercentage: new Big(0), hasErrors: true, initialValue: new Big(0), netPerformance: new Big(0), netPerformancePercentage: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0) + netPerformanceWithCurrencyEffect: new Big(0), + netPerformanceWithCurrencyEffectPercentage: new Big(0) }; } - let averagePriceAtEndDate = new Big(0); - let averagePriceAtStartDate = new Big(0); - const currentValues: { [date: string]: Big } = {}; - let feesAtStartDate = new Big(0); - let fees = new Big(0); - let grossPerformance = new Big(0); - let grossPerformanceAtStartDate = new Big(0); - let grossPerformanceFromSells = new Big(0); - let initialValue: Big; - let investmentAtStartDate: Big; - const investmentValues: { [date: string]: Big } = {}; - const maxInvestmentValues: { [date: string]: Big } = {}; - let lastAveragePrice = new Big(0); - let maxTotalInvestment = new Big(0); - const netPerformanceValues: { [date: string]: Big } = {}; - const timeWeightedInvestmentValues: { [date: string]: Big } = {}; - let totalInvestment = new Big(0); - let totalInvestmentWithGrossPerformanceFromSell = new Big(0); - let totalUnits = new Big(0); - let valueAtStartDate: Big; - // Add a synthetic order at the start and the end date orders.push({ symbol, @@ -1120,6 +1314,7 @@ export class PortfolioCalculator { let totalInvestmentDays = 0; let sumOfTimeWeightedInvestments = new Big(0); + let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); for (let i = 0; i < orders.length; i += 1) { const order = orders[i]; @@ -1130,6 +1325,14 @@ export class PortfolioCalculator { console.log(i + 1, order.type, order.itemType); } + const exchangeRateAtOrderDate = exchangeRates[order.date]; + + if (!exchangeRateAtOrderDate) { + console.error( + `${symbol}: No exchange rate found for date ${order.date}` + ); + } + if (order.itemType === 'start') { // Take the unit price of the order as the market price if there are no // orders of this symbol before the start date @@ -1139,27 +1342,44 @@ export class PortfolioCalculator { : unitPriceAtStartDate; } - // Calculate the average start price as soon as any units are held - if ( - averagePriceAtStartDate.eq(0) && - i >= indexOfStartOrder && - totalUnits.gt(0) - ) { - averagePriceAtStartDate = totalInvestment.div(totalUnits); + if (order.unitPrice) { + order.unitPriceInBaseCurrency = order.unitPrice.mul( + currentExchangeRate ?? 1 + ); + + order.unitPriceInBaseCurrencyWithCurrencyEffect = order.unitPrice.mul( + exchangeRateAtOrderDate ?? 1 + ); } + // TODO: + // Do all order types have a unit price? What about dividends that + // are defined with an order quantity of 1 and the unit price of the total + // dividend? const valueOfInvestmentBeforeTransaction = totalUnits.mul( - order.unitPrice + order.unitPriceInBaseCurrency ); + const valueOfInvestmentBeforeTransactionWithCurrencyEffect = + totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); + if (!investmentAtStartDate && i >= indexOfStartOrder) { investmentAtStartDate = totalInvestment ?? new Big(0); + + investmentAtStartDateWithCurrencyEffect = + totalInvestmentWithCurrencyEffect ?? new Big(0); + valueAtStartDate = valueOfInvestmentBeforeTransaction; + + valueAtStartDateWithCurrencyEffect = + valueOfInvestmentBeforeTransactionWithCurrencyEffect; } const transactionInvestment = order.type === 'BUY' - ? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type)) + ? order.quantity + .mul(order.unitPriceInBaseCurrency) + .mul(this.getFactor(order.type)) : totalUnits.gt(0) ? totalInvestment .div(totalUnits) @@ -1167,22 +1387,46 @@ export class PortfolioCalculator { .mul(this.getFactor(order.type)) : new Big(0); + const transactionInvestmentWithCurrencyEffect = + order.type === 'BUY' + ? order.quantity + .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) + .mul(this.getFactor(order.type)) + : totalUnits.gt(0) + ? totalInvestmentWithCurrencyEffect + .div(totalUnits) + .mul(order.quantity) + .mul(this.getFactor(order.type)) + : new Big(0); + if (PortfolioCalculator.ENABLE_LOGGING) { console.log('totalInvestment', totalInvestment.toNumber()); + + console.log( + 'totalInvestmentWithCurrencyEffect', + totalInvestmentWithCurrencyEffect.toNumber() + ); + console.log('order.quantity', order.quantity.toNumber()); console.log('transactionInvestment', transactionInvestment.toNumber()); + + console.log( + 'transactionInvestmentWithCurrencyEffect', + transactionInvestmentWithCurrencyEffect.toNumber() + ); } const totalInvestmentBeforeTransaction = totalInvestment; - totalInvestment = totalInvestment.plus(transactionInvestment); - if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) { - maxTotalInvestment = totalInvestment; - } + const totalInvestmentBeforeTransactionWithCurrencyEffect = + totalInvestmentWithCurrencyEffect; - if (i === indexOfEndOrder && totalUnits.gt(0)) { - averagePriceAtEndDate = totalInvestment.div(totalUnits); - } + totalInvestment = totalInvestment.plus(transactionInvestment); + + totalInvestmentWithCurrencyEffect = + totalInvestmentWithCurrencyEffect.plus( + transactionInvestmentWithCurrencyEffect + ); if (i >= indexOfStartOrder && !initialValue) { if ( @@ -1190,57 +1434,116 @@ export class PortfolioCalculator { !valueOfInvestmentBeforeTransaction.eq(0) ) { initialValue = valueOfInvestmentBeforeTransaction; + + initialValueWithCurrencyEffect = + valueOfInvestmentBeforeTransactionWithCurrencyEffect; } else if (transactionInvestment.gt(0)) { initialValue = transactionInvestment; + + initialValueWithCurrencyEffect = + transactionInvestmentWithCurrencyEffect; } } - fees = fees.plus(order.fee); + fees = fees.plus(order.fee.mul(currentExchangeRate ?? 1)); + + feesWithCurrencyEffect = feesWithCurrencyEffect.plus( + order.fee.mul(exchangeRateAtOrderDate ?? 1) + ); totalUnits = totalUnits.plus( order.quantity.mul(this.getFactor(order.type)) ); - const valueOfInvestment = totalUnits.mul(order.unitPrice); + const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); + + const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( + order.unitPriceInBaseCurrencyWithCurrencyEffect + ); const grossPerformanceFromSell = order.type === TypeOfOrder.SELL - ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) + ? order.unitPriceInBaseCurrency + .minus(lastAveragePrice) + .mul(order.quantity) + : new Big(0); + + const grossPerformanceFromSellWithCurrencyEffect = + order.type === TypeOfOrder.SELL + ? order.unitPriceInBaseCurrencyWithCurrencyEffect + .minus(lastAveragePriceWithCurrencyEffect) + .mul(order.quantity) : new Big(0); grossPerformanceFromSells = grossPerformanceFromSells.plus( grossPerformanceFromSell ); + grossPerformanceFromSellsWithCurrencyEffect = + grossPerformanceFromSellsWithCurrencyEffect.plus( + grossPerformanceFromSellWithCurrencyEffect + ); + totalInvestmentWithGrossPerformanceFromSell = totalInvestmentWithGrossPerformanceFromSell .plus(transactionInvestment) .plus(grossPerformanceFromSell); + totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect = + totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect + .plus(transactionInvestmentWithCurrencyEffect) + .plus(grossPerformanceFromSellWithCurrencyEffect); + lastAveragePrice = totalUnits.eq(0) ? new Big(0) : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); + lastAveragePriceWithCurrencyEffect = totalUnits.eq(0) + ? new Big(0) + : totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.div( + totalUnits + ); + if (PortfolioCalculator.ENABLE_LOGGING) { console.log( 'totalInvestmentWithGrossPerformanceFromSell', totalInvestmentWithGrossPerformanceFromSell.toNumber() ); + console.log( + 'totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect', + totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.toNumber() + ); console.log( 'grossPerformanceFromSells', grossPerformanceFromSells.toNumber() ); + console.log( + 'grossPerformanceFromSellWithCurrencyEffect', + grossPerformanceFromSellWithCurrencyEffect.toNumber() + ); } const newGrossPerformance = valueOfInvestment .minus(totalInvestment) .plus(grossPerformanceFromSells); + const newGrossPerformanceWithCurrencyEffect = + valueOfInvestmentWithCurrencyEffect + .minus(totalInvestmentWithCurrencyEffect) + .plus(grossPerformanceFromSellsWithCurrencyEffect); + grossPerformance = newGrossPerformance; + grossPerformanceWithCurrencyEffect = + newGrossPerformanceWithCurrencyEffect; + if (order.itemType === 'start') { feesAtStartDate = fees; + feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; grossPerformanceAtStartDate = grossPerformance; + + grossPerformanceAtStartDateWithCurrencyEffect = + grossPerformanceWithCurrencyEffect; } if (i > indexOfStartOrder) { @@ -1272,30 +1575,71 @@ export class PortfolioCalculator { .plus(totalInvestmentBeforeTransaction) .mul(daysSinceLastOrder) ); + + sumOfTimeWeightedInvestmentsWithCurrencyEffect = + sumOfTimeWeightedInvestmentsWithCurrencyEffect.add( + valueAtStartDateWithCurrencyEffect + .minus(investmentAtStartDateWithCurrencyEffect) + .plus(totalInvestmentBeforeTransactionWithCurrencyEffect) + .mul(daysSinceLastOrder) + ); } if (isChartMode) { currentValues[order.date] = valueOfInvestment; + + currentValuesWithCurrencyEffect[order.date] = + valueOfInvestmentWithCurrencyEffect; + netPerformanceValues[order.date] = grossPerformance .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); + netPerformanceValuesWithCurrencyEffect[order.date] = + grossPerformanceWithCurrencyEffect + .minus(grossPerformanceAtStartDateWithCurrencyEffect) + .minus( + feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) + ); + investmentValues[order.date] = totalInvestment; - maxInvestmentValues[order.date] = maxTotalInvestment; + + investmentValuesWithCurrencyEffect[order.date] = + totalInvestmentWithCurrencyEffect; timeWeightedInvestmentValues[order.date] = totalInvestmentDays > 0 ? sumOfTimeWeightedInvestments.div(totalInvestmentDays) : new Big(0); + + timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = + totalInvestmentDays > 0 + ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( + totalInvestmentDays + ) + : new Big(0); } } if (PortfolioCalculator.ENABLE_LOGGING) { console.log('totalInvestment', totalInvestment.toNumber()); + + console.log( + 'totalInvestmentWithCurrencyEffect', + totalInvestmentWithCurrencyEffect.toNumber() + ); + console.log( 'totalGrossPerformance', grossPerformance.minus(grossPerformanceAtStartDate).toNumber() ); + + console.log( + 'totalGrossPerformanceWithCurrencyEffect', + grossPerformanceWithCurrencyEffect + .minus(grossPerformanceAtStartDateWithCurrencyEffect) + .toNumber() + ); } if (i === indexOfEndOrder) { @@ -1307,83 +1651,73 @@ export class PortfolioCalculator { grossPerformanceAtStartDate ); + const totalGrossPerformanceWithCurrencyEffect = + grossPerformanceWithCurrencyEffect.minus( + grossPerformanceAtStartDateWithCurrencyEffect + ); + const totalNetPerformance = grossPerformance .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); + const totalNetPerformanceWithCurrencyEffect = + grossPerformanceWithCurrencyEffect + .minus(grossPerformanceAtStartDateWithCurrencyEffect) + .minus(feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)); + const timeWeightedAverageInvestmentBetweenStartAndEndDate = totalInvestmentDays > 0 ? sumOfTimeWeightedInvestments.div(totalInvestmentDays) : new Big(0); - const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( - maxTotalInvestment.minus(investmentAtStartDate) - ); + const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect = + totalInvestmentDays > 0 + ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( + totalInvestmentDays + ) + : new Big(0); - let grossPerformancePercentage: Big; + const grossPerformancePercentage = + timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) + ? totalGrossPerformance.div( + timeWeightedAverageInvestmentBetweenStartAndEndDate + ) + : new Big(0); - if ( - PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT - ) { - grossPerformancePercentage = - timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) - ? totalGrossPerformance.div( - timeWeightedAverageInvestmentBetweenStartAndEndDate - ) - : new Big(0); - } else { - grossPerformancePercentage = - averagePriceAtStartDate.eq(0) || - averagePriceAtEndDate.eq(0) || - orders[indexOfStartOrder].unitPrice.eq(0) - ? maxInvestmentBetweenStartAndEndDate.gt(0) - ? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate) - : new Big(0) - : // This formula has the issue that buying more units with a price - // lower than the average buying price results in a positive - // performance even if the market price stays constant - unitPriceAtEndDate - .div(averagePriceAtEndDate) - .div( - orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) - ) - .minus(1); - } + const grossPerformancePercentageWithCurrencyEffect = + timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt( + 0 + ) + ? totalGrossPerformanceWithCurrencyEffect.div( + timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect + ) + : new Big(0); const feesPerUnit = totalUnits.gt(0) ? fees.minus(feesAtStartDate).div(totalUnits) : new Big(0); - let netPerformancePercentage: Big; + const feesPerUnitWithCurrencyEffect = totalUnits.gt(0) + ? feesWithCurrencyEffect + .minus(feesAtStartDateWithCurrencyEffect) + .div(totalUnits) + : new Big(0); - if ( - PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT - ) { - netPerformancePercentage = - timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) - ? totalNetPerformance.div( - timeWeightedAverageInvestmentBetweenStartAndEndDate - ) - : new Big(0); - } else { - netPerformancePercentage = - averagePriceAtStartDate.eq(0) || - averagePriceAtEndDate.eq(0) || - orders[indexOfStartOrder].unitPrice.eq(0) - ? maxInvestmentBetweenStartAndEndDate.gt(0) - ? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate) - : new Big(0) - : // This formula has the issue that buying more units with a price - // lower than the average buying price results in a positive - // performance even if the market price stays constant - unitPriceAtEndDate - .minus(feesPerUnit) - .div(averagePriceAtEndDate) - .div( - orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) - ) - .minus(1); - } + const netPerformancePercentage = + timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) + ? totalNetPerformance.div( + timeWeightedAverageInvestmentBetweenStartAndEndDate + ) + : new Big(0); + + const netPerformancePercentageWithCurrencyEffect = + timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt( + 0 + ) + ? totalNetPerformanceWithCurrencyEffect.div( + timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect + ) + : new Big(0); if (PortfolioCalculator.ENABLE_LOGGING) { console.log( @@ -1392,38 +1726,64 @@ export class PortfolioCalculator { Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( 2 )} -> ${unitPriceAtEndDate.toFixed(2)} - Average price: ${averagePriceAtStartDate.toFixed( - 2 - )} -> ${averagePriceAtEndDate.toFixed(2)} Total investment: ${totalInvestment.toFixed(2)} + Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed( + 2 + )} Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed( 2 )} - Max. total investment: ${maxTotalInvestment.toFixed(2)} + Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( + 2 + )} Gross performance: ${totalGrossPerformance.toFixed( 2 )} / ${grossPerformancePercentage.mul(100).toFixed(2)}% + Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed( + 2 + )} / ${grossPerformancePercentageWithCurrencyEffect + .mul(100) + .toFixed(2)}% Fees per unit: ${feesPerUnit.toFixed(2)} + Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( + 2 + )} Net performance: ${totalNetPerformance.toFixed( 2 - )} / ${netPerformancePercentage.mul(100).toFixed(2)}%` + )} / ${netPerformancePercentage.mul(100).toFixed(2)}% + Net performance with currency effect: ${totalNetPerformanceWithCurrencyEffect.toFixed( + 2 + )} / ${netPerformancePercentageWithCurrencyEffect.mul(100).toFixed(2)}%` ); } return { currentValues, + currentValuesWithCurrencyEffect, grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, initialValue, + initialValueWithCurrencyEffect, investmentValues, - maxInvestmentValues, + investmentValuesWithCurrencyEffect, netPerformancePercentage, + netPerformancePercentageWithCurrencyEffect, netPerformanceValues, + netPerformanceValuesWithCurrencyEffect, timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect, + totalInvestment, + totalInvestmentWithCurrencyEffect, grossPerformance: totalGrossPerformance, + grossPerformanceWithCurrencyEffect: + totalGrossPerformanceWithCurrencyEffect, hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), netPerformance: totalNetPerformance, + netPerformanceWithCurrencyEffect: totalNetPerformanceWithCurrencyEffect, timeWeightedInvestment: - timeWeightedAverageInvestmentBetweenStartAndEndDate + timeWeightedAverageInvestmentBetweenStartAndEndDate, + timeWeightedInvestmentWithCurrencyEffect: + timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index cd4fa0bae..a68b66e4f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -285,6 +285,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator({ currency: this.request.user.Settings.settings.baseCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -407,6 +408,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator({ currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -704,6 +706,8 @@ export class PortfolioService { firstBuyDate: undefined, grossPerformance: undefined, grossPerformancePercent: undefined, + grossPerformancePercentWithCurrencyEffect: undefined, + grossPerformanceWithCurrencyEffect: undefined, historicalData: [], investment: undefined, marketPrice: undefined, @@ -711,6 +715,8 @@ export class PortfolioService { minPrice: undefined, netPerformance: undefined, netPerformancePercent: undefined, + netPerformancePercentWithCurrencyEffect: undefined, + netPerformanceWithCurrencyEffect: undefined, orders: [], quantity: undefined, SymbolProfile: undefined, @@ -719,7 +725,6 @@ export class PortfolioService { }; } - const positionCurrency = orders[0].SymbolProfile.currency; const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ { dataSource: aDataSource, symbol: aSymbol } ]); @@ -746,8 +751,9 @@ export class PortfolioService { tags = uniqBy(tags, 'id'); const portfolioCalculator = new PortfolioCalculator({ - currency: positionCurrency, + currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -755,6 +761,7 @@ export class PortfolioService { const transactionPoints = portfolioCalculator.getTransactionPoints(); const portfolioStart = parseDate(transactionPoints[0].date); + const currentPositions = await portfolioCalculator.getCurrentPositions(portfolioStart); @@ -784,23 +791,6 @@ export class PortfolioService { }) ); - // Convert investment, gross and net performance to currency of user - const investment = this.exchangeRateDataService.toCurrency( - position.investment?.toNumber(), - currency, - userCurrency - ); - const grossPerformance = this.exchangeRateDataService.toCurrency( - position.grossPerformance?.toNumber(), - currency, - userCurrency - ); - const netPerformance = this.exchangeRateDataService.toCurrency( - position.netPerformance?.toNumber(), - currency, - userCurrency - ); - const historicalData = await this.dataProviderService.getHistorical( [{ dataSource, symbol: aSymbol }], 'day', @@ -865,12 +855,9 @@ export class PortfolioService { return { firstBuyDate, - grossPerformance, - investment, marketPrice, maxPrice, minPrice, - netPerformance, orders, SymbolProfile, tags, @@ -883,10 +870,21 @@ export class PortfolioService { SymbolProfile.currency, userCurrency ), + grossPerformance: position.grossPerformance?.toNumber(), grossPerformancePercent: position.grossPerformancePercentage?.toNumber(), + grossPerformancePercentWithCurrencyEffect: + position.grossPerformancePercentageWithCurrencyEffect?.toNumber(), + grossPerformanceWithCurrencyEffect: + position.grossPerformanceWithCurrencyEffect?.toNumber(), historicalData: historicalDataArray, + investment: position.investment?.toNumber(), + netPerformance: position.netPerformance?.toNumber(), netPerformancePercent: position.netPerformancePercentage?.toNumber(), + netPerformancePercentWithCurrencyEffect: + position.netPerformancePercentageWithCurrencyEffect?.toNumber(), + netPerformanceWithCurrencyEffect: + position.netPerformanceWithCurrencyEffect?.toNumber(), quantity: quantity.toNumber(), value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice ?? 0).toNumber(), @@ -945,10 +943,14 @@ export class PortfolioService { firstBuyDate: undefined, grossPerformance: undefined, grossPerformancePercent: undefined, + grossPerformancePercentWithCurrencyEffect: undefined, + grossPerformanceWithCurrencyEffect: undefined, historicalData: historicalDataArray, investment: 0, netPerformance: undefined, netPerformancePercent: undefined, + netPerformancePercentWithCurrencyEffect: undefined, + netPerformanceWithCurrencyEffect: undefined, quantity: 0, transactionCount: undefined, value: 0 @@ -986,6 +988,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator({ currency: this.request.user.Settings.settings.baseCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -1017,6 +1020,7 @@ export class PortfolioService { ]); const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; + for (const symbolProfile of symbolProfiles) { symbolProfileMap[symbolProfile.symbol] = symbolProfile; } @@ -1040,12 +1044,18 @@ export class PortfolioService { averagePrice, currency, dataSource, + // fee, firstBuyDate, - investment, grossPerformance, grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, + grossPerformanceWithCurrencyEffect, + investment, + investmentWithCurrencyEffect, netPerformance, netPerformancePercentage, + netPerformancePercentageWithCurrencyEffect, + netPerformanceWithCurrencyEffect, quantity, symbol, transactionCount @@ -1059,17 +1069,32 @@ export class PortfolioService { assetClass: symbolProfileMap[symbol].assetClass, assetSubClass: symbolProfileMap[symbol].assetSubClass, averagePrice: averagePrice.toNumber(), + // fee: fee?.toNumber(), grossPerformance: grossPerformance?.toNumber() ?? null, grossPerformancePercentage: grossPerformancePercentage?.toNumber() ?? null, + grossPerformancePercentageWithCurrencyEffect: + grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? null, + grossPerformanceWithCurrencyEffect: + grossPerformanceWithCurrencyEffect?.toNumber() ?? null, investment: investment.toNumber(), + investmentWithCurrencyEffect: + investmentWithCurrencyEffect?.toNumber(), marketState: dataProviderResponses[symbol]?.marketState ?? 'delayed', name: symbolProfileMap[symbol].name, netPerformance: netPerformance?.toNumber() ?? null, netPerformancePercentage: netPerformancePercentage?.toNumber() ?? null, + netPerformancePercentageWithCurrencyEffect: + netPerformancePercentageWithCurrencyEffect?.toNumber() ?? null, + netPerformanceWithCurrencyEffect: + netPerformanceWithCurrencyEffect?.toNumber() ?? null, quantity: quantity.toNumber() + /*quantity: quantity?.toNumber(), + timeWeightedInvestment: timeWeightedInvestment?.toNumber(), + timeWeightedInvestmentWithCurrencyEffect: + timeWeightedInvestmentWithCurrencyEffect?.toNumber()*/ }; } ) @@ -1128,6 +1153,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator({ currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -1139,8 +1165,12 @@ export class PortfolioService { performance: { currentGrossPerformance: 0, currentGrossPerformancePercent: 0, + currentGrossPerformancePercentWithCurrencyEffect: 0, + currentGrossPerformanceWithCurrencyEffect: 0, currentNetPerformance: 0, currentNetPerformancePercent: 0, + currentNetPerformancePercentWithCurrencyEffect: 0, + currentNetPerformanceWithCurrencyEffect: 0, currentNetWorth: 0, currentValue: 0, totalInvestment: 0 @@ -1165,17 +1195,26 @@ export class PortfolioService { errors, grossPerformance, grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, + grossPerformanceWithCurrencyEffect, hasErrors, netPerformance, netPerformancePercentage, + netPerformancePercentageWithCurrencyEffect, + netPerformanceWithCurrencyEffect, totalInvestment } = await portfolioCalculator.getCurrentPositions(startDate); - const currentGrossPerformance = grossPerformance; - const currentGrossPerformancePercent = grossPerformancePercentage; let currentNetPerformance = netPerformance; + let currentNetPerformancePercent = netPerformancePercentage; + let currentNetPerformancePercentWithCurrencyEffect = + netPerformancePercentageWithCurrencyEffect; + + let currentNetPerformanceWithCurrencyEffect = + netPerformanceWithCurrencyEffect; + const { items } = await this.getChart({ dateRange, impersonationId, @@ -1191,9 +1230,18 @@ export class PortfolioService { if (itemOfToday) { currentNetPerformance = new Big(itemOfToday.netPerformance); + + currentNetPerformanceWithCurrencyEffect = new Big( + itemOfToday.netPerformanceWithCurrencyEffect + ); + currentNetPerformancePercent = new Big( itemOfToday.netPerformanceInPercentage ).div(100); + + currentNetPerformancePercentWithCurrencyEffect = new Big( + itemOfToday.netPerformanceInPercentageWithCurrencyEffect + ).div(100); } accountBalanceItems = accountBalanceItems.filter(({ date }) => { @@ -1226,11 +1274,18 @@ export class PortfolioService { firstOrderDate: parseDate(items[0]?.date), performance: { currentNetWorth, - currentGrossPerformance: currentGrossPerformance.toNumber(), - currentGrossPerformancePercent: - currentGrossPerformancePercent.toNumber(), + currentGrossPerformance: grossPerformance.toNumber(), + currentGrossPerformancePercent: grossPerformancePercentage.toNumber(), + currentGrossPerformancePercentWithCurrencyEffect: + grossPerformancePercentageWithCurrencyEffect.toNumber(), + currentGrossPerformanceWithCurrencyEffect: + grossPerformanceWithCurrencyEffect.toNumber(), currentNetPerformance: currentNetPerformance.toNumber(), currentNetPerformancePercent: currentNetPerformancePercent.toNumber(), + currentNetPerformancePercentWithCurrencyEffect: + currentNetPerformancePercentWithCurrencyEffect.toNumber(), + currentNetPerformanceWithCurrencyEffect: + currentNetPerformanceWithCurrencyEffect.toNumber(), currentValue: currentValue.toNumber(), totalInvestment: totalInvestment.toNumber() } @@ -1250,6 +1305,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator({ currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -1413,6 +1469,7 @@ export class PortfolioService { const portfolioCalculator = new PortfolioCalculator({ currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); @@ -1757,6 +1814,7 @@ export class PortfolioService { const annualizedPerformancePercent = new PortfolioCalculator({ currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: [] }) .getAnnualizedPerformancePercent({ @@ -1866,30 +1924,19 @@ export class PortfolioService { currency: order.SymbolProfile.currency, dataSource: order.SymbolProfile.dataSource, date: format(order.date, DATE_FORMAT), - fee: new Big( - this.exchangeRateDataService.toCurrency( - order.fee, - order.SymbolProfile.currency, - userCurrency - ) - ), + fee: new Big(order.fee), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), symbol: order.SymbolProfile.symbol, tags: order.tags, type: order.type, - unitPrice: new Big( - this.exchangeRateDataService.toCurrency( - order.unitPrice, - order.SymbolProfile.currency, - userCurrency - ) - ) + unitPrice: new Big(order.unitPrice) })); const portfolioCalculator = new PortfolioCalculator({ currency: userCurrency, currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, orders: portfolioOrders }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts new file mode 100644 index 000000000..f1c8728a3 --- /dev/null +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -0,0 +1,19 @@ +export const ExchangeRateDataServiceMock = { + getExchangeRatesByCurrency: ({ + currencies, + endDate, + startDate, + targetCurrency + }): Promise => { + return Promise.resolve({ + CHF: { + '2023-01-03': 1, + '2023-07-10': 1 + }, + USD: { + '2023-01-03': 0.92285, + '2023-07-10': 0.8889 + } + }); + } +}; diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 5ddef02a5..7389a3ee8 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -7,9 +7,13 @@ import { DEFAULT_CURRENCY, PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; -import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getYesterday, + resetHours +} from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; -import { format, isToday } from 'date-fns'; +import { addDays, format, isAfter, isToday } from 'date-fns'; import { isNumber, uniq } from 'lodash'; import ms from 'ms'; @@ -34,6 +38,59 @@ export class ExchangeRateDataService { return this.currencyPairs; } + public async getExchangeRatesByCurrency({ + currencies, + endDate = new Date(), + startDate, + targetCurrency + }: { + currencies: string[]; + endDate?: Date; + startDate: Date; + targetCurrency: string; + }) { + if (!startDate) { + return {}; + } + + let exchangeRatesByCurrency: { + [currency: string]: { [dateString: string]: number }; + } = {}; + + let dates: Date[] = []; + let currentDate = resetHours(startDate); + + while (isAfter(endDate, currentDate)) { + dates.push(currentDate); + currentDate = addDays(currentDate, 1); + } + + for (let currency of uniq(Object.values(currencies))) { + exchangeRatesByCurrency[currency] = await this.getExchangeRates({ + dates, + currencyFrom: currency, + currencyTo: targetCurrency + }); + + let previousExchangeRate = 1; + + let date = startDate; + do { + let dateString = format(date, DATE_FORMAT); + + if (isNaN(exchangeRatesByCurrency[currency][dateString])) { + exchangeRatesByCurrency[currency][dateString] = previousExchangeRate; + } else { + previousExchangeRate = exchangeRatesByCurrency[currency][dateString]; + } + + date = addDays(resetHours(date), 1); + } while (!isAfter(date, endDate)); + } + + return exchangeRatesByCurrency; + } + public async getExchangeRates({ currencyFrom, currencyTo, diff --git a/libs/common/src/lib/interfaces/historical-data-item.interface.ts b/libs/common/src/lib/interfaces/historical-data-item.interface.ts index b348e33aa..18238ee36 100644 --- a/libs/common/src/lib/interfaces/historical-data-item.interface.ts +++ b/libs/common/src/lib/interfaces/historical-data-item.interface.ts @@ -4,12 +4,16 @@ export interface HistoricalDataItem { grossPerformancePercent?: number; marketPrice?: number; netPerformance?: number; + netPerformanceWithCurrencyEffect?: number; netPerformanceInPercentage?: number; + netPerformanceInPercentageWithCurrencyEffect?: number; netWorth?: number; netWorthInPercentage?: number; quantity?: number; totalAccountBalance?: number; totalInvestment?: number; + totalInvestmentValueWithCurrencyEffect?: number; value?: number; + valueWithCurrencyEffect?: number; valueInPercentage?: number; } diff --git a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts index 0343ef338..1c6f50b30 100644 --- a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts @@ -2,8 +2,12 @@ export interface PortfolioPerformance { annualizedPerformancePercent?: number; currentGrossPerformance: number; currentGrossPerformancePercent: number; + currentGrossPerformancePercentWithCurrencyEffect: number; + currentGrossPerformanceWithCurrencyEffect: number; currentNetPerformance: number; currentNetPerformancePercent: number; + currentNetPerformancePercentWithCurrencyEffect: number; + currentNetPerformanceWithCurrencyEffect: number; currentNetWorth: number; currentValue: number; totalInvestment: number; diff --git a/libs/common/src/lib/interfaces/timeline-position.interface.ts b/libs/common/src/lib/interfaces/timeline-position.interface.ts index 1b27de8dc..220a0aa8f 100644 --- a/libs/common/src/lib/interfaces/timeline-position.interface.ts +++ b/libs/common/src/lib/interfaces/timeline-position.interface.ts @@ -9,13 +9,20 @@ export interface TimelinePosition { firstBuyDate: string; grossPerformance: Big; grossPerformancePercentage: Big; + grossPerformancePercentageWithCurrencyEffect: Big; + grossPerformanceWithCurrencyEffect: Big; investment: Big; + investmentWithCurrencyEffect: Big; marketPrice: number; + marketPriceInBaseCurrency: number; netPerformance: Big; netPerformancePercentage: Big; + netPerformancePercentageWithCurrencyEffect: Big; + netPerformanceWithCurrencyEffect: Big; quantity: Big; symbol: string; tags?: Tag[]; timeWeightedInvestment: Big; + timeWeightedInvestmentWithCurrencyEffect: Big; transactionCount: number; }