Feature/Add exchange rate effects to portfolio calculation (#2834)
* Add exchange rate effects to portfolio calculation * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
parent
a72e98f73c
commit
be801b481e
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Prepared the portfolio calculation for exchange rate effects
|
||||
|
||||
## 2.37.0 - 2024-01-11
|
||||
|
||||
### Changed
|
||||
|
@ -235,27 +235,17 @@ export class BenchmarkService {
|
||||
})
|
||||
]);
|
||||
|
||||
const exchangeRates = await this.exchangeRateDataService.getExchangeRates({
|
||||
currencyFrom: currentSymbolItem.currency,
|
||||
currencyTo: userCurrency,
|
||||
dates: marketDataItems.map(({ date }) => {
|
||||
return date;
|
||||
})
|
||||
const exchangeRates =
|
||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||
startDate,
|
||||
currencies: [currentSymbolItem.currency],
|
||||
targetCurrency: userCurrency
|
||||
});
|
||||
|
||||
const exchangeRateAtStartDate =
|
||||
exchangeRates[format(startDate, DATE_FORMAT)];
|
||||
|
||||
if (!exchangeRateAtStartDate) {
|
||||
Logger.error(
|
||||
`No exchange rate has been found for ${
|
||||
currentSymbolItem.currency
|
||||
}${userCurrency} at ${format(startDate, DATE_FORMAT)}`,
|
||||
'BenchmarkService'
|
||||
);
|
||||
|
||||
return { marketData };
|
||||
}
|
||||
exchangeRates[currentSymbolItem.currency]?.[
|
||||
format(startDate, DATE_FORMAT)
|
||||
];
|
||||
|
||||
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
|
||||
return isSameDay(date, startDate);
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -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<GetValuesObject>({
|
||||
dataProviderInfos: [],
|
||||
@ -137,7 +126,7 @@ describe('CurrentRateService', () => {
|
||||
{
|
||||
dataSource: 'YAHOO',
|
||||
date: undefined,
|
||||
marketPriceInBaseCurrency: 1841.823902,
|
||||
marketPrice: 1841.823902,
|
||||
symbol: 'AMZN'
|
||||
}
|
||||
]
|
||||
|
@ -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<GetValuesObject> {
|
||||
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
|
||||
)
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface GetValueObject extends UniqueAsset {
|
||||
date: Date;
|
||||
marketPriceInBaseCurrency: number;
|
||||
marketPrice: number;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import Big from 'big.js';
|
||||
|
||||
import { PortfolioOrder } from './portfolio-order.interface';
|
||||
|
||||
export interface PortfolioOrderItem extends PortfolioOrder {
|
||||
itemType?: '' | 'start' | 'end';
|
||||
unitPriceInBaseCurrency?: Big;
|
||||
unitPriceInBaseCurrencyWithCurrencyEffect?: Big;
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ export interface PortfolioPositionDetail {
|
||||
firstBuyDate: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
grossPerformancePercentWithCurrencyEffect: 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;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
@ -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([
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
@ -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([
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
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';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
@ -14,21 +16,42 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||
};
|
||||
});
|
||||
|
||||
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, 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: [
|
||||
{
|
||||
currency: 'CHF',
|
||||
currency: 'USD',
|
||||
date: '2015-01-01',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big(0),
|
||||
@ -39,7 +62,7 @@ describe('PortfolioCalculator', () => {
|
||||
unitPrice: new Big(320.43)
|
||||
},
|
||||
{
|
||||
currency: 'CHF',
|
||||
currency: 'USD',
|
||||
date: '2017-12-31',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big(0),
|
||||
@ -70,33 +93,60 @@ describe('PortfolioCalculator', () => {
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
currentValue: new Big('13657.2'),
|
||||
currentValue: new Big('13298.425356'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('27172.74'),
|
||||
grossPerformancePercentage: new Big('42.41978276196153750666'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'41.6401219622042072686'
|
||||
),
|
||||
grossPerformanceWithCurrencyEffect: new Big('26516.208701400000064086'),
|
||||
hasErrors: false,
|
||||
netPerformance: new Big('27172.74'),
|
||||
netPerformancePercentage: new Big('42.41978276196153750666'),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'41.6401219622042072686'
|
||||
),
|
||||
netPerformanceWithCurrencyEffect: new Big('26516.208701400000064086'),
|
||||
positions: [
|
||||
{
|
||||
averagePrice: new Big('320.43'),
|
||||
currency: 'CHF',
|
||||
currency: 'USD',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big('0'),
|
||||
firstBuyDate: '2015-01-01',
|
||||
grossPerformance: new Big('27172.74'),
|
||||
grossPerformancePercentage: new Big('42.41978276196153750666'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'41.6401219622042072686'
|
||||
),
|
||||
grossPerformanceWithCurrencyEffect: new Big(
|
||||
'26516.208701400000064086'
|
||||
),
|
||||
investment: new Big('320.43'),
|
||||
investmentWithCurrencyEffect: new Big('318.542667299999967957'),
|
||||
marketPrice: 13657.2,
|
||||
marketPriceInBaseCurrency: 13298.425356,
|
||||
netPerformance: new Big('27172.74'),
|
||||
netPerformancePercentage: new Big('42.41978276196153750666'),
|
||||
marketPrice: 13657.2,
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'41.6401219622042072686'
|
||||
),
|
||||
netPerformanceWithCurrencyEffect: new Big(
|
||||
'26516.208701400000064086'
|
||||
),
|
||||
quantity: new Big('1'),
|
||||
symbol: 'BTCUSD',
|
||||
tags: undefined,
|
||||
timeWeightedInvestment: new Big('640.56763686131386861314'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big(
|
||||
'636.79469348020066587024'
|
||||
),
|
||||
transactionCount: 2
|
||||
}
|
||||
],
|
||||
totalInvestment: new Big('320.43')
|
||||
totalInvestment: new Big('320.43'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -0,0 +1,144 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
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';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||
import { PortfolioCalculator } from './portfolio-calculator';
|
||||
|
||||
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.10483'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('27.33'),
|
||||
grossPerformancePercentage: new Big('0.3066651705565529623'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'0.25235044599563974109'
|
||||
),
|
||||
grossPerformanceWithCurrencyEffect: new Big('20.775774'),
|
||||
hasErrors: false,
|
||||
netPerformance: new Big('26.33'),
|
||||
netPerformancePercentage: new Big('0.29544434470377019749'),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'0.24112962014285697628'
|
||||
),
|
||||
netPerformanceWithCurrencyEffect: new Big('19.851974'),
|
||||
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.25235044599563974109'
|
||||
),
|
||||
grossPerformanceWithCurrencyEffect: new Big('20.775774'),
|
||||
investment: new Big('89.12'),
|
||||
investmentWithCurrencyEffect: new Big('82.329056'),
|
||||
netPerformance: new Big('26.33'),
|
||||
netPerformancePercentage: new Big('0.29544434470377019749'),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'0.24112962014285697628'
|
||||
),
|
||||
netPerformanceWithCurrencyEffect: new Big('19.851974'),
|
||||
marketPrice: 116.45,
|
||||
marketPriceInBaseCurrency: 103.10483,
|
||||
quantity: new Big('1'),
|
||||
symbol: 'GOOGL',
|
||||
tags: undefined,
|
||||
timeWeightedInvestment: new Big('89.12'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
|
||||
transactionCount: 1
|
||||
}
|
||||
],
|
||||
totalInvestment: new Big('89.12'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('82.329056')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
{ date: '2023-01-03', investment: new Big('89.12') }
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2023-01-01', investment: new Big('89.12') }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,5 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
@ -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)
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
@ -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'),
|
||||
@ -86,17 +104,31 @@ describe('PortfolioCalculator', () => {
|
||||
firstBuyDate: '2022-03-07',
|
||||
grossPerformance: new Big('21.93'),
|
||||
grossPerformancePercentage: new Big('0.15113417083448194384'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(
|
||||
'0.15113417083448194384'
|
||||
),
|
||||
grossPerformanceWithCurrencyEffect: new Big('21.93'),
|
||||
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([
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import Big from 'big.js';
|
||||
|
||||
@ -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([
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import Big from 'big.js';
|
||||
|
||||
import { CurrentRateService } from './current-rate.service';
|
||||
@ -5,14 +6,23 @@ import { PortfolioCalculator } from './portfolio-calculator';
|
||||
|
||||
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: []
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
});
|
||||
|
||||
@ -480,7 +482,7 @@ export class PortfolioService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = item.quantity.mul(item.marketPrice ?? 0);
|
||||
const value = item.quantity.mul(item.marketPriceInBaseCurrency ?? 0);
|
||||
const symbolProfile = symbolProfileMap[item.symbol];
|
||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -1041,13 +1045,20 @@ export class PortfolioService {
|
||||
currency,
|
||||
dataSource,
|
||||
firstBuyDate,
|
||||
investment,
|
||||
grossPerformance,
|
||||
grossPerformancePercentage,
|
||||
grossPerformancePercentageWithCurrencyEffect,
|
||||
grossPerformanceWithCurrencyEffect,
|
||||
investment,
|
||||
investmentWithCurrencyEffect,
|
||||
netPerformance,
|
||||
netPerformancePercentage,
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
quantity,
|
||||
symbol,
|
||||
timeWeightedInvestment,
|
||||
timeWeightedInvestmentWithCurrencyEffect,
|
||||
transactionCount
|
||||
}) => {
|
||||
return {
|
||||
@ -1062,14 +1073,27 @@ export class PortfolioService {
|
||||
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,
|
||||
quantity: quantity.toNumber()
|
||||
netPerformancePercentageWithCurrencyEffect:
|
||||
netPerformancePercentageWithCurrencyEffect?.toNumber() ?? null,
|
||||
netPerformanceWithCurrencyEffect:
|
||||
netPerformanceWithCurrencyEffect?.toNumber() ?? null,
|
||||
quantity: quantity.toNumber(),
|
||||
timeWeightedInvestment: timeWeightedInvestment?.toNumber(),
|
||||
timeWeightedInvestmentWithCurrencyEffect:
|
||||
timeWeightedInvestmentWithCurrencyEffect?.toNumber()
|
||||
};
|
||||
}
|
||||
)
|
||||
@ -1128,6 +1152,7 @@ export class PortfolioService {
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: userCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
exchangeRateDataService: this.exchangeRateDataService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
|
||||
@ -1139,8 +1164,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 +1194,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 +1229,18 @@ export class PortfolioService {
|
||||
|
||||
if (itemOfToday) {
|
||||
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
||||
|
||||
currentNetPerformancePercent = new Big(
|
||||
itemOfToday.netPerformanceInPercentage
|
||||
).div(100);
|
||||
|
||||
currentNetPerformancePercentWithCurrencyEffect = new Big(
|
||||
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
|
||||
).div(100);
|
||||
|
||||
currentNetPerformanceWithCurrencyEffect = new Big(
|
||||
itemOfToday.netPerformanceWithCurrencyEffect
|
||||
);
|
||||
}
|
||||
|
||||
accountBalanceItems = accountBalanceItems.filter(({ date }) => {
|
||||
@ -1226,11 +1273,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 +1304,7 @@ export class PortfolioService {
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: userCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
exchangeRateDataService: this.exchangeRateDataService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
|
||||
@ -1413,6 +1468,7 @@ export class PortfolioService {
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: userCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
exchangeRateDataService: this.exchangeRateDataService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
|
||||
@ -1757,6 +1813,7 @@ export class PortfolioService {
|
||||
const annualizedPerformancePercent = new PortfolioCalculator({
|
||||
currency: userCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
exchangeRateDataService: this.exchangeRateDataService,
|
||||
orders: []
|
||||
})
|
||||
.getAnnualizedPerformancePercent({
|
||||
@ -1866,30 +1923,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
|
||||
});
|
||||
|
||||
@ -2025,7 +2071,8 @@ export class PortfolioService {
|
||||
for (const order of ordersByAccount) {
|
||||
let currentValueOfSymbolInBaseCurrency =
|
||||
order.quantity *
|
||||
(portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ??
|
||||
(portfolioItemsNow[order.SymbolProfile.symbol]
|
||||
?.marketPriceInBaseCurrency ??
|
||||
order.unitPrice ??
|
||||
0);
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
export const ExchangeRateDataServiceMock = {
|
||||
getExchangeRatesByCurrency: ({
|
||||
currencies,
|
||||
endDate,
|
||||
startDate,
|
||||
targetCurrency
|
||||
}): Promise<any> => {
|
||||
if (targetCurrency === 'CHF') {
|
||||
return Promise.resolve({
|
||||
CHF: {
|
||||
'2015-01-01': 1,
|
||||
'2017-12-31': 1,
|
||||
'2018-01-01': 1,
|
||||
'2023-01-03': 1,
|
||||
'2023-07-10': 1
|
||||
},
|
||||
USD: {
|
||||
'2015-01-01': 0.9941099999999999,
|
||||
'2017-12-31': 0.9787,
|
||||
'2018-01-01': 0.97373,
|
||||
'2023-01-03': 0.9238,
|
||||
'2023-07-10': 0.8854
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
}
|
||||
};
|
@ -7,9 +7,19 @@ 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 {
|
||||
eachDayOfInterval,
|
||||
format,
|
||||
isBefore,
|
||||
isToday,
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import { isNumber, uniq } from 'lodash';
|
||||
import ms from 'ms';
|
||||
|
||||
@ -34,123 +44,60 @@ export class ExchangeRateDataService {
|
||||
return this.currencyPairs;
|
||||
}
|
||||
|
||||
public async getExchangeRates({
|
||||
currencyFrom,
|
||||
currencyTo,
|
||||
dates
|
||||
public async getExchangeRatesByCurrency({
|
||||
currencies,
|
||||
endDate = new Date(),
|
||||
startDate,
|
||||
targetCurrency
|
||||
}: {
|
||||
currencyFrom: string;
|
||||
currencyTo: string;
|
||||
dates: Date[];
|
||||
currencies: string[];
|
||||
endDate?: Date;
|
||||
startDate: Date;
|
||||
targetCurrency: string;
|
||||
}) {
|
||||
let factors: { [dateString: string]: number } = {};
|
||||
|
||||
if (currencyFrom === currencyTo) {
|
||||
for (const date of dates) {
|
||||
factors[format(date, DATE_FORMAT)] = 1;
|
||||
if (!startDate) {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
const dataSource =
|
||||
this.dataProviderService.getDataSourceForExchangeRates();
|
||||
const symbol = `${currencyFrom}${currencyTo}`;
|
||||
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: { in: dates },
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource,
|
||||
symbol
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (marketData?.length > 0) {
|
||||
for (const { date, marketPrice } of marketData) {
|
||||
factors[format(date, DATE_FORMAT)] = marketPrice;
|
||||
}
|
||||
} else {
|
||||
// Calculate indirectly via base currency
|
||||
|
||||
let marketPriceBaseCurrencyFromCurrency: {
|
||||
[dateString: string]: number;
|
||||
} = {};
|
||||
let marketPriceBaseCurrencyToCurrency: {
|
||||
[dateString: string]: number;
|
||||
let exchangeRatesByCurrency: {
|
||||
[currency: string]: { [dateString: string]: number };
|
||||
} = {};
|
||||
|
||||
try {
|
||||
if (currencyFrom === DEFAULT_CURRENCY) {
|
||||
for (const date of dates) {
|
||||
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
|
||||
1;
|
||||
}
|
||||
} else {
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: { in: dates },
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource,
|
||||
symbol: `${DEFAULT_CURRENCY}${currencyFrom}`
|
||||
}
|
||||
]
|
||||
for (let currency of currencies) {
|
||||
exchangeRatesByCurrency[currency] = await this.getExchangeRates({
|
||||
startDate,
|
||||
currencyFrom: currency,
|
||||
currencyTo: targetCurrency
|
||||
});
|
||||
|
||||
for (const { date, marketPrice } of marketData) {
|
||||
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
|
||||
marketPrice;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
let previousExchangeRate = 1;
|
||||
|
||||
try {
|
||||
if (currencyTo === DEFAULT_CURRENCY) {
|
||||
for (const date of dates) {
|
||||
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1;
|
||||
}
|
||||
} else {
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: {
|
||||
in: dates
|
||||
},
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource,
|
||||
symbol: `${DEFAULT_CURRENCY}${currencyTo}`
|
||||
}
|
||||
]
|
||||
});
|
||||
// Start from the most recent date and fill in missing exchange rates
|
||||
// using the latest available rate
|
||||
for (
|
||||
let date = endDate;
|
||||
!isBefore(date, startDate);
|
||||
date = subDays(resetHours(date), 1)
|
||||
) {
|
||||
let dateString = format(date, DATE_FORMAT);
|
||||
|
||||
for (const { date, marketPrice } of marketData) {
|
||||
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] =
|
||||
marketPrice;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
// Check if the exchange rate for the current date is missing
|
||||
if (isNaN(exchangeRatesByCurrency[currency][dateString])) {
|
||||
// If missing, fill with the previous exchange rate
|
||||
exchangeRatesByCurrency[currency][dateString] = previousExchangeRate;
|
||||
|
||||
for (const date of dates) {
|
||||
try {
|
||||
const factor =
|
||||
(1 /
|
||||
marketPriceBaseCurrencyFromCurrency[
|
||||
format(date, DATE_FORMAT)
|
||||
]) *
|
||||
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)];
|
||||
|
||||
factors[format(date, DATE_FORMAT)] = factor;
|
||||
} catch {
|
||||
Logger.error(
|
||||
`No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
|
||||
date,
|
||||
DATE_FORMAT
|
||||
)}`,
|
||||
`No exchange rate has been found for ${DEFAULT_CURRENCY}${targetCurrency} at ${dateString}`,
|
||||
'ExchangeRateDataService'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If available, update the previous exchange rate
|
||||
previousExchangeRate = exchangeRatesByCurrency[currency][dateString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return factors;
|
||||
return exchangeRatesByCurrency;
|
||||
}
|
||||
|
||||
public hasCurrencyPair(currency1: string, currency2: string) {
|
||||
@ -396,6 +343,129 @@ export class ExchangeRateDataService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getExchangeRates({
|
||||
currencyFrom,
|
||||
currencyTo,
|
||||
endDate = new Date(),
|
||||
startDate
|
||||
}: {
|
||||
currencyFrom: string;
|
||||
currencyTo: string;
|
||||
endDate?: Date;
|
||||
startDate: Date;
|
||||
}) {
|
||||
const dates = eachDayOfInterval({ end: endDate, start: startDate });
|
||||
let factors: { [dateString: string]: number } = {};
|
||||
|
||||
if (currencyFrom === currencyTo) {
|
||||
for (const date of dates) {
|
||||
factors[format(date, DATE_FORMAT)] = 1;
|
||||
}
|
||||
} else {
|
||||
const dataSource =
|
||||
this.dataProviderService.getDataSourceForExchangeRates();
|
||||
const symbol = `${currencyFrom}${currencyTo}`;
|
||||
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: { gte: startDate, lt: endDate },
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource,
|
||||
symbol
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (marketData?.length > 0) {
|
||||
for (const { date, marketPrice } of marketData) {
|
||||
factors[format(date, DATE_FORMAT)] = marketPrice;
|
||||
}
|
||||
} else {
|
||||
// Calculate indirectly via base currency
|
||||
|
||||
let marketPriceBaseCurrencyFromCurrency: {
|
||||
[dateString: string]: number;
|
||||
} = {};
|
||||
let marketPriceBaseCurrencyToCurrency: {
|
||||
[dateString: string]: number;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
if (currencyFrom === DEFAULT_CURRENCY) {
|
||||
for (const date of dates) {
|
||||
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
|
||||
1;
|
||||
}
|
||||
} else {
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: { gte: startDate, lt: endDate },
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource,
|
||||
symbol: `${DEFAULT_CURRENCY}${currencyFrom}`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
for (const { date, marketPrice } of marketData) {
|
||||
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
|
||||
marketPrice;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (currencyTo === DEFAULT_CURRENCY) {
|
||||
for (const date of dates) {
|
||||
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1;
|
||||
}
|
||||
} else {
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource,
|
||||
symbol: `${DEFAULT_CURRENCY}${currencyTo}`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
for (const { date, marketPrice } of marketData) {
|
||||
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] =
|
||||
marketPrice;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
for (const date of dates) {
|
||||
try {
|
||||
const factor =
|
||||
(1 /
|
||||
marketPriceBaseCurrencyFromCurrency[
|
||||
format(date, DATE_FORMAT)
|
||||
]) *
|
||||
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)];
|
||||
|
||||
factors[format(date, DATE_FORMAT)] = factor;
|
||||
} catch {
|
||||
Logger.error(
|
||||
`No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
|
||||
date,
|
||||
DATE_FORMAT
|
||||
)}`,
|
||||
'ExchangeRateDataService'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return factors;
|
||||
}
|
||||
|
||||
private async prepareCurrencies(): Promise<string[]> {
|
||||
let currencies: string[] = [];
|
||||
|
||||
|
@ -5,11 +5,15 @@ export interface HistoricalDataItem {
|
||||
marketPrice?: number;
|
||||
netPerformance?: number;
|
||||
netPerformanceInPercentage?: number;
|
||||
netPerformanceInPercentageWithCurrencyEffect?: number;
|
||||
netPerformanceWithCurrencyEffect?: number;
|
||||
netWorth?: number;
|
||||
netWorthInPercentage?: number;
|
||||
quantity?: number;
|
||||
totalAccountBalance?: number;
|
||||
totalInvestment?: number;
|
||||
totalInvestmentValueWithCurrencyEffect?: number;
|
||||
value?: number;
|
||||
valueInPercentage?: number;
|
||||
valueWithCurrencyEffect?: number;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user