optimize portfolio calculator to fetch all symbols for one day
This commit is contained in:
parent
cdc8faff7f
commit
aabfb39e8f
@ -1,6 +1,7 @@
|
||||
import {
|
||||
CurrentRateService,
|
||||
GetValueParams
|
||||
GetValueParams,
|
||||
GetValuesParams
|
||||
} from '@ghostfolio/api/app/core/current-rate.service';
|
||||
import {
|
||||
PortfolioCalculator,
|
||||
@ -11,7 +12,14 @@ import {
|
||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||
import { Currency } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { differenceInCalendarDays, parse } from 'date-fns';
|
||||
import {
|
||||
addDays,
|
||||
differenceInCalendarDays,
|
||||
endOfDay,
|
||||
isBefore,
|
||||
parse
|
||||
} from 'date-fns';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
|
||||
function toYearMonthDay(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
@ -32,6 +40,27 @@ function dateEqual(date1: Date, date2: Date) {
|
||||
);
|
||||
}
|
||||
|
||||
function mockGetValue(symbol: string, date: Date) {
|
||||
const today = new Date();
|
||||
if (symbol === 'VTI') {
|
||||
if (dateEqual(today, date)) {
|
||||
return { marketPrice: 213.32 };
|
||||
} else {
|
||||
const startDate = parse('2019-02-01', 'yyyy-MM-dd', new Date());
|
||||
const daysInBetween = differenceInCalendarDays(date, startDate);
|
||||
|
||||
const marketPrice = new Big('144.38').plus(
|
||||
new Big('0.08').mul(daysInBetween)
|
||||
);
|
||||
return { marketPrice: marketPrice.toNumber() };
|
||||
}
|
||||
} else if (symbol === 'AMZN') {
|
||||
return { marketPrice: 2021.99 };
|
||||
} else {
|
||||
return { marketPrice: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('@ghostfolio/api/app/core/current-rate.service', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@ -43,24 +72,30 @@ jest.mock('@ghostfolio/api/app/core/current-rate.service', () => {
|
||||
currency,
|
||||
userCurrency
|
||||
}: GetValueParams) => {
|
||||
const today = new Date();
|
||||
if (symbol === 'VTI') {
|
||||
if (dateEqual(today, date)) {
|
||||
return Promise.resolve({ marketPrice: new Big('213.32') });
|
||||
} else {
|
||||
const startDate = parse('2019-02-01', 'yyyy-MM-dd', new Date());
|
||||
const daysInBetween = differenceInCalendarDays(date, startDate);
|
||||
|
||||
const marketPrice = new Big('144.38').plus(
|
||||
new Big('0.08').mul(daysInBetween)
|
||||
);
|
||||
return Promise.resolve({ marketPrice });
|
||||
return Promise.resolve(mockGetValue(symbol, date));
|
||||
},
|
||||
getValues: ({
|
||||
currencies,
|
||||
dateRangeEnd,
|
||||
dateRangeStart,
|
||||
symbols,
|
||||
userCurrency
|
||||
}: GetValuesParams) => {
|
||||
const result = [];
|
||||
for (
|
||||
let date = resetHours(dateRangeStart);
|
||||
isBefore(date, endOfDay(dateRangeEnd));
|
||||
date = addDays(date, 1)
|
||||
) {
|
||||
for (const symbol of symbols) {
|
||||
result.push({
|
||||
date,
|
||||
symbol,
|
||||
marketPrice: mockGetValue(symbol, date).marketPrice
|
||||
});
|
||||
}
|
||||
} else if (symbol === 'AMZN') {
|
||||
return Promise.resolve({ marketPrice: new Big('2021.99') });
|
||||
}
|
||||
|
||||
return Promise.resolve({ marketPrice: new Big('0') });
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
};
|
||||
})
|
||||
@ -545,7 +580,7 @@ describe('PortfolioCalculator', () => {
|
||||
quantity: new Big('25'),
|
||||
symbol: 'VTI',
|
||||
investment: new Big('4460.95'),
|
||||
marketPrice: new Big('213.32'),
|
||||
marketPrice: 213.32,
|
||||
transactionCount: 5,
|
||||
grossPerformance: new Big('872.05'), // 213.32*25-4460.95
|
||||
grossPerformancePercentage: new Big('0.19548526659119694236') // 872.05/4460.95
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
||||
import {
|
||||
CurrentRateService,
|
||||
GetValueObject
|
||||
} from '@ghostfolio/api/app/core/current-rate.service';
|
||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||
import { Currency } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
@ -11,6 +14,7 @@ import {
|
||||
isBefore,
|
||||
parse
|
||||
} from 'date-fns';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
@ -198,38 +202,65 @@ export class PortfolioCalculator {
|
||||
currentDate: Date
|
||||
): Promise<TimelinePeriod> {
|
||||
let investment: Big = new Big(0);
|
||||
const promises = [];
|
||||
|
||||
let value = new Big(0);
|
||||
const currentDateAsString = format(currentDate, DATE_FORMAT);
|
||||
if (j >= 0) {
|
||||
const currencies: { [name: string]: Currency } = {};
|
||||
const symbols: string[] = [];
|
||||
|
||||
for (const item of this.transactionPoints[j].items) {
|
||||
currencies[item.symbol] = item.currency;
|
||||
symbols.push(item.symbol);
|
||||
investment = investment.add(item.investment);
|
||||
promises.push(
|
||||
this.currentRateService
|
||||
.getValue({
|
||||
date: currentDate,
|
||||
symbol: item.symbol,
|
||||
currency: item.currency,
|
||||
userCurrency: this.currency
|
||||
})
|
||||
.then(({ marketPrice }) => new Big(marketPrice).mul(item.quantity))
|
||||
}
|
||||
|
||||
let marketSymbols: GetValueObject[] = [];
|
||||
if (symbols.length > 0) {
|
||||
try {
|
||||
marketSymbols = await this.currentRateService.getValues({
|
||||
dateRangeStart: resetHours(currentDate),
|
||||
dateRangeEnd: resetHours(currentDate),
|
||||
symbols,
|
||||
currencies,
|
||||
userCurrency: this.currency
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`failed to fetch info for date ${currentDate} with exception`,
|
||||
e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const marketSymbolMap: {
|
||||
[date: string]: { [symbol: string]: Big };
|
||||
} = {};
|
||||
for (const marketSymbol of marketSymbols) {
|
||||
const date = format(marketSymbol.date, DATE_FORMAT);
|
||||
if (!marketSymbolMap[date]) {
|
||||
marketSymbolMap[date] = {};
|
||||
}
|
||||
marketSymbolMap[date][marketSymbol.symbol] = new Big(
|
||||
marketSymbol.marketPrice
|
||||
);
|
||||
}
|
||||
|
||||
for (const item of this.transactionPoints[j].items) {
|
||||
if (
|
||||
!marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
value = value.add(
|
||||
item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Promise.all(promises).catch((e) => {
|
||||
console.error(
|
||||
`failed to fetch info for date ${currentDate} with exception`,
|
||||
e
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = result.reduce((a, b) => a.add(b), new Big(0));
|
||||
return {
|
||||
date: format(currentDate, DATE_FORMAT),
|
||||
date: currentDateAsString,
|
||||
grossPerformance: value.minus(investment),
|
||||
investment,
|
||||
value
|
||||
@ -310,9 +341,9 @@ export interface TimelineSpecification {
|
||||
|
||||
export interface TimelinePeriod {
|
||||
date: string;
|
||||
grossPerformance: number;
|
||||
grossPerformance: Big;
|
||||
investment: Big;
|
||||
value: number;
|
||||
value: Big;
|
||||
}
|
||||
|
||||
export interface PortfolioOrder {
|
||||
|
Loading…
x
Reference in New Issue
Block a user