Browse Source

optimize portfolio calculator to fetch all symbols for one day

pull/239/head
Valentin Zickner 4 years ago
committed by Thomas
parent
commit
aabfb39e8f
  1. 73
      apps/api/src/app/core/portfolio-calculator.spec.ts
  2. 83
      apps/api/src/app/core/portfolio-calculator.ts

73
apps/api/src/app/core/portfolio-calculator.spec.ts

@ -1,6 +1,7 @@
import { import {
CurrentRateService, CurrentRateService,
GetValueParams GetValueParams,
GetValuesParams
} from '@ghostfolio/api/app/core/current-rate.service'; } from '@ghostfolio/api/app/core/current-rate.service';
import { import {
PortfolioCalculator, PortfolioCalculator,
@ -11,7 +12,14 @@ import {
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import Big from 'big.js'; 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) { function toYearMonthDay(date: Date) {
const year = date.getFullYear(); 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', () => { jest.mock('@ghostfolio/api/app/core/current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@ -43,24 +72,30 @@ jest.mock('@ghostfolio/api/app/core/current-rate.service', () => {
currency, currency,
userCurrency userCurrency
}: GetValueParams) => { }: GetValueParams) => {
const today = new Date(); return Promise.resolve(mockGetValue(symbol, date));
if (symbol === 'VTI') { },
if (dateEqual(today, date)) { getValues: ({
return Promise.resolve({ marketPrice: new Big('213.32') }); currencies,
} else { dateRangeEnd,
const startDate = parse('2019-02-01', 'yyyy-MM-dd', new Date()); dateRangeStart,
const daysInBetween = differenceInCalendarDays(date, startDate); symbols,
userCurrency
const marketPrice = new Big('144.38').plus( }: GetValuesParams) => {
new Big('0.08').mul(daysInBetween) const result = [];
); for (
return Promise.resolve({ marketPrice }); 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(result);
return Promise.resolve({ marketPrice: new Big('0') });
} }
}; };
}) })
@ -545,7 +580,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('25'), quantity: new Big('25'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
marketPrice: new Big('213.32'), marketPrice: 213.32,
transactionCount: 5, transactionCount: 5,
grossPerformance: new Big('872.05'), // 213.32*25-4460.95 grossPerformance: new Big('872.05'), // 213.32*25-4460.95
grossPerformancePercentage: new Big('0.19548526659119694236') // 872.05/4460.95 grossPerformancePercentage: new Big('0.19548526659119694236') // 872.05/4460.95

83
apps/api/src/app/core/portfolio-calculator.ts

@ -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 { OrderType } from '@ghostfolio/api/models/order-type';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
@ -11,6 +14,7 @@ import {
isBefore, isBefore,
parse parse
} from 'date-fns'; } from 'date-fns';
import { resetHours } from '@ghostfolio/common/helper';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
@ -198,38 +202,65 @@ export class PortfolioCalculator {
currentDate: Date currentDate: Date
): Promise<TimelinePeriod> { ): Promise<TimelinePeriod> {
let investment: Big = new Big(0); let investment: Big = new Big(0);
const promises = [];
let value = new Big(0);
const currentDateAsString = format(currentDate, DATE_FORMAT);
if (j >= 0) { if (j >= 0) {
const currencies: { [name: string]: Currency } = {};
const symbols: string[] = [];
for (const item of this.transactionPoints[j].items) { for (const item of this.transactionPoints[j].items) {
currencies[item.symbol] = item.currency;
symbols.push(item.symbol);
investment = investment.add(item.investment); 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))
);
} }
}
const result = await Promise.all(promises).catch((e) => { let marketSymbols: GetValueObject[] = [];
console.error( if (symbols.length > 0) {
`failed to fetch info for date ${currentDate} with exception`, try {
e marketSymbols = await this.currentRateService.getValues({
); dateRangeStart: resetHours(currentDate),
return null; dateRangeEnd: resetHours(currentDate),
}); symbols,
currencies,
userCurrency: this.currency
});
} catch (e) {
console.error(
`failed to fetch info for date ${currentDate} with exception`,
e
);
return null;
}
}
if (result == null) { const marketSymbolMap: {
return null; [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 value = result.reduce((a, b) => a.add(b), new Big(0));
return { return {
date: format(currentDate, DATE_FORMAT), date: currentDateAsString,
grossPerformance: value.minus(investment), grossPerformance: value.minus(investment),
investment, investment,
value value
@ -310,9 +341,9 @@ export interface TimelineSpecification {
export interface TimelinePeriod { export interface TimelinePeriod {
date: string; date: string;
grossPerformance: number; grossPerformance: Big;
investment: Big; investment: Big;
value: number; value: Big;
} }
export interface PortfolioOrder { export interface PortfolioOrder {

Loading…
Cancel
Save