optimize portfolio calculator to fetch all symbols for one day
This commit is contained in:
parent
cdc8faff7f
commit
aabfb39e8f
@ -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
|
||||||
|
@ -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({
|
let marketSymbols: GetValueObject[] = [];
|
||||||
date: currentDate,
|
if (symbols.length > 0) {
|
||||||
symbol: item.symbol,
|
try {
|
||||||
currency: item.currency,
|
marketSymbols = await this.currentRateService.getValues({
|
||||||
userCurrency: this.currency
|
dateRangeStart: resetHours(currentDate),
|
||||||
})
|
dateRangeEnd: resetHours(currentDate),
|
||||||
.then(({ marketPrice }) => new Big(marketPrice).mul(item.quantity))
|
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 {
|
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…
x
Reference in New Issue
Block a user