|
|
|
@ -19,13 +19,14 @@ import {
|
|
|
|
|
import {
|
|
|
|
|
AssetProfileIdentifier,
|
|
|
|
|
DataProviderInfo,
|
|
|
|
|
Filter,
|
|
|
|
|
HistoricalDataItem,
|
|
|
|
|
InvestmentItem,
|
|
|
|
|
ResponseError,
|
|
|
|
|
SymbolMetrics
|
|
|
|
|
} from '@ghostfolio/common/interfaces';
|
|
|
|
|
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
|
|
|
|
|
import { DateRange, GroupBy } from '@ghostfolio/common/types';
|
|
|
|
|
import { GroupBy } from '@ghostfolio/common/types';
|
|
|
|
|
|
|
|
|
|
import { Logger } from '@nestjs/common';
|
|
|
|
|
import { Big } from 'big.js';
|
|
|
|
@ -37,12 +38,10 @@ import {
|
|
|
|
|
format,
|
|
|
|
|
isAfter,
|
|
|
|
|
isBefore,
|
|
|
|
|
isSameDay,
|
|
|
|
|
max,
|
|
|
|
|
min,
|
|
|
|
|
subDays
|
|
|
|
|
} from 'date-fns';
|
|
|
|
|
import { first, last, uniq, uniqBy } from 'lodash';
|
|
|
|
|
import { first, isNumber, last, sortBy, sum, uniq, uniqBy } from 'lodash';
|
|
|
|
|
|
|
|
|
|
export abstract class PortfolioCalculator {
|
|
|
|
|
protected static readonly ENABLE_LOGGING = false;
|
|
|
|
@ -54,15 +53,14 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
private currency: string;
|
|
|
|
|
private currentRateService: CurrentRateService;
|
|
|
|
|
private dataProviderInfos: DataProviderInfo[];
|
|
|
|
|
private dateRange: DateRange;
|
|
|
|
|
private endDate: Date;
|
|
|
|
|
private exchangeRateDataService: ExchangeRateDataService;
|
|
|
|
|
private filters: Filter[];
|
|
|
|
|
private redisCacheService: RedisCacheService;
|
|
|
|
|
private snapshot: PortfolioSnapshot;
|
|
|
|
|
private snapshotPromise: Promise<void>;
|
|
|
|
|
private startDate: Date;
|
|
|
|
|
private transactionPoints: TransactionPoint[];
|
|
|
|
|
private useCache: boolean;
|
|
|
|
|
private userId: string;
|
|
|
|
|
|
|
|
|
|
public constructor({
|
|
|
|
@ -71,10 +69,9 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
configurationService,
|
|
|
|
|
currency,
|
|
|
|
|
currentRateService,
|
|
|
|
|
dateRange,
|
|
|
|
|
exchangeRateDataService,
|
|
|
|
|
filters,
|
|
|
|
|
redisCacheService,
|
|
|
|
|
useCache,
|
|
|
|
|
userId
|
|
|
|
|
}: {
|
|
|
|
|
accountBalanceItems: HistoricalDataItem[];
|
|
|
|
@ -82,18 +79,19 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
configurationService: ConfigurationService;
|
|
|
|
|
currency: string;
|
|
|
|
|
currentRateService: CurrentRateService;
|
|
|
|
|
dateRange: DateRange;
|
|
|
|
|
exchangeRateDataService: ExchangeRateDataService;
|
|
|
|
|
filters: Filter[];
|
|
|
|
|
redisCacheService: RedisCacheService;
|
|
|
|
|
useCache: boolean;
|
|
|
|
|
userId: string;
|
|
|
|
|
}) {
|
|
|
|
|
this.accountBalanceItems = accountBalanceItems;
|
|
|
|
|
this.configurationService = configurationService;
|
|
|
|
|
this.currency = currency;
|
|
|
|
|
this.currentRateService = currentRateService;
|
|
|
|
|
this.dateRange = dateRange;
|
|
|
|
|
this.exchangeRateDataService = exchangeRateDataService;
|
|
|
|
|
this.filters = filters;
|
|
|
|
|
|
|
|
|
|
let dateOfFirstActivity = new Date();
|
|
|
|
|
|
|
|
|
|
this.activities = activities
|
|
|
|
|
.map(
|
|
|
|
@ -106,10 +104,14 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
type,
|
|
|
|
|
unitPrice
|
|
|
|
|
}) => {
|
|
|
|
|
if (isAfter(date, new Date(Date.now()))) {
|
|
|
|
|
if (isBefore(date, dateOfFirstActivity)) {
|
|
|
|
|
dateOfFirstActivity = date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isAfter(date, new Date())) {
|
|
|
|
|
// Adapt date to today if activity is in future (e.g. liability)
|
|
|
|
|
// to include it in the interval
|
|
|
|
|
date = endOfDay(new Date(Date.now()));
|
|
|
|
|
date = endOfDay(new Date());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
@ -128,10 +130,12 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.redisCacheService = redisCacheService;
|
|
|
|
|
this.useCache = useCache;
|
|
|
|
|
this.userId = userId;
|
|
|
|
|
|
|
|
|
|
const { endDate, startDate } = getIntervalFromDateRange(dateRange);
|
|
|
|
|
const { endDate, startDate } = getIntervalFromDateRange(
|
|
|
|
|
'max',
|
|
|
|
|
subDays(dateOfFirstActivity, 1)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.endDate = endDate;
|
|
|
|
|
this.startDate = startDate;
|
|
|
|
@ -145,38 +149,18 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
positions: TimelinePosition[]
|
|
|
|
|
): PortfolioSnapshot;
|
|
|
|
|
|
|
|
|
|
public async computeSnapshot(
|
|
|
|
|
start: Date,
|
|
|
|
|
end?: Date
|
|
|
|
|
): Promise<PortfolioSnapshot> {
|
|
|
|
|
private async computeSnapshot(): Promise<PortfolioSnapshot> {
|
|
|
|
|
const lastTransactionPoint = last(this.transactionPoints);
|
|
|
|
|
|
|
|
|
|
let endDate = end;
|
|
|
|
|
|
|
|
|
|
if (!endDate) {
|
|
|
|
|
endDate = new Date(Date.now());
|
|
|
|
|
|
|
|
|
|
if (lastTransactionPoint) {
|
|
|
|
|
endDate = max([endDate, parseDate(lastTransactionPoint.date)]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
|
|
|
|
|
return isBefore(parseDate(date), endDate);
|
|
|
|
|
return isBefore(parseDate(date), this.endDate);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!transactionPoints.length) {
|
|
|
|
|
return {
|
|
|
|
|
currentValueInBaseCurrency: 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),
|
|
|
|
|
historicalData: [],
|
|
|
|
|
positions: [],
|
|
|
|
|
totalFeesWithCurrencyEffect: new Big(0),
|
|
|
|
|
totalInterestWithCurrencyEffect: new Big(0),
|
|
|
|
@ -189,15 +173,12 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
|
|
|
|
|
const currencies: { [symbol: string]: string } = {};
|
|
|
|
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
|
|
|
|
let dates: Date[] = [];
|
|
|
|
|
let firstIndex = transactionPoints.length;
|
|
|
|
|
let firstTransactionPoint: TransactionPoint = null;
|
|
|
|
|
let totalInterestWithCurrencyEffect = new Big(0);
|
|
|
|
|
let totalLiabilitiesWithCurrencyEffect = new Big(0);
|
|
|
|
|
let totalValuablesWithCurrencyEffect = new Big(0);
|
|
|
|
|
|
|
|
|
|
dates.push(resetHours(start));
|
|
|
|
|
|
|
|
|
|
for (const { currency, dataSource, symbol } of transactionPoints[
|
|
|
|
|
firstIndex - 1
|
|
|
|
|
].items) {
|
|
|
|
@ -211,47 +192,19 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < transactionPoints.length; i++) {
|
|
|
|
|
if (
|
|
|
|
|
!isBefore(parseDate(transactionPoints[i].date), start) &&
|
|
|
|
|
!isBefore(parseDate(transactionPoints[i].date), this.startDate) &&
|
|
|
|
|
firstTransactionPoint === null
|
|
|
|
|
) {
|
|
|
|
|
firstTransactionPoint = transactionPoints[i];
|
|
|
|
|
firstIndex = i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (firstTransactionPoint !== null) {
|
|
|
|
|
dates.push(resetHours(parseDate(transactionPoints[i].date)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dates.push(resetHours(endDate));
|
|
|
|
|
|
|
|
|
|
// Add dates of last week for fallback
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 7));
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 6));
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 5));
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 4));
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 3));
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 2));
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 1));
|
|
|
|
|
dates.push(resetHours(new Date()));
|
|
|
|
|
|
|
|
|
|
dates = uniq(
|
|
|
|
|
dates.map((date) => {
|
|
|
|
|
return date.getTime();
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.map((timestamp) => {
|
|
|
|
|
return new Date(timestamp);
|
|
|
|
|
})
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
return a.getTime() - b.getTime();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let exchangeRatesByCurrency =
|
|
|
|
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
|
|
|
|
currencies: uniq(Object.values(currencies)),
|
|
|
|
|
endDate: endOfDay(endDate),
|
|
|
|
|
startDate: this.getStartDate(),
|
|
|
|
|
endDate: endOfDay(this.endDate),
|
|
|
|
|
startDate: this.startDate,
|
|
|
|
|
targetCurrency: this.currency
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -262,7 +215,8 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
} = await this.currentRateService.getValues({
|
|
|
|
|
dataGatheringItems,
|
|
|
|
|
dateQuery: {
|
|
|
|
|
in: dates
|
|
|
|
|
gte: this.startDate,
|
|
|
|
|
lt: this.endDate
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -286,7 +240,19 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const endDateString = format(endDate, DATE_FORMAT);
|
|
|
|
|
const endDateString = format(this.endDate, DATE_FORMAT);
|
|
|
|
|
|
|
|
|
|
const daysInMarket = differenceInDays(this.endDate, this.startDate);
|
|
|
|
|
|
|
|
|
|
let chartDateMap = this.getChartDateMap({
|
|
|
|
|
endDate: this.endDate,
|
|
|
|
|
startDate: this.startDate,
|
|
|
|
|
step: Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const chartDates = sortBy(Object.keys(chartDateMap), (chartDate) => {
|
|
|
|
|
return chartDate;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (firstIndex > 0) {
|
|
|
|
|
firstIndex--;
|
|
|
|
@ -297,6 +263,35 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
|
|
|
|
|
const errors: ResponseError['errors'] = [];
|
|
|
|
|
|
|
|
|
|
const accumulatedValuesByDate: {
|
|
|
|
|
[date: string]: {
|
|
|
|
|
investmentValueWithCurrencyEffect: Big;
|
|
|
|
|
totalAccountBalanceWithCurrencyEffect: 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 };
|
|
|
|
|
investmentValuesAccumulated: { [date: string]: Big };
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect: { [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 item of lastTransactionPoint.items) {
|
|
|
|
|
const feeInBaseCurrency = item.fee.mul(
|
|
|
|
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
|
|
|
@ -313,16 +308,25 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
currentValues,
|
|
|
|
|
currentValuesWithCurrencyEffect,
|
|
|
|
|
grossPerformance,
|
|
|
|
|
grossPerformancePercentage,
|
|
|
|
|
grossPerformancePercentageWithCurrencyEffect,
|
|
|
|
|
grossPerformanceWithCurrencyEffect,
|
|
|
|
|
hasErrors,
|
|
|
|
|
investmentValuesAccumulated,
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect,
|
|
|
|
|
investmentValuesWithCurrencyEffect,
|
|
|
|
|
netPerformance,
|
|
|
|
|
netPerformancePercentage,
|
|
|
|
|
netPerformancePercentageWithCurrencyEffect,
|
|
|
|
|
netPerformanceWithCurrencyEffect,
|
|
|
|
|
netPerformancePercentageWithCurrencyEffectMap,
|
|
|
|
|
netPerformanceValues,
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect,
|
|
|
|
|
netPerformanceWithCurrencyEffectMap,
|
|
|
|
|
timeWeightedInvestment,
|
|
|
|
|
timeWeightedInvestmentValues,
|
|
|
|
|
timeWeightedInvestmentValuesWithCurrencyEffect,
|
|
|
|
|
timeWeightedInvestmentWithCurrencyEffect,
|
|
|
|
|
totalDividend,
|
|
|
|
|
totalDividendInBaseCurrency,
|
|
|
|
@ -332,17 +336,30 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
totalLiabilitiesInBaseCurrency,
|
|
|
|
|
totalValuablesInBaseCurrency
|
|
|
|
|
} = this.getSymbolMetrics({
|
|
|
|
|
chartDateMap,
|
|
|
|
|
marketSymbolMap,
|
|
|
|
|
start,
|
|
|
|
|
dataSource: item.dataSource,
|
|
|
|
|
end: endDate,
|
|
|
|
|
end: this.endDate,
|
|
|
|
|
exchangeRates:
|
|
|
|
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
|
|
|
|
start: this.startDate,
|
|
|
|
|
symbol: item.symbol
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
|
|
|
|
|
|
|
|
|
valuesBySymbol[item.symbol] = {
|
|
|
|
|
currentValues,
|
|
|
|
|
currentValuesWithCurrencyEffect,
|
|
|
|
|
investmentValuesAccumulated,
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect,
|
|
|
|
|
investmentValuesWithCurrencyEffect,
|
|
|
|
|
netPerformanceValues,
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect,
|
|
|
|
|
timeWeightedInvestmentValues,
|
|
|
|
|
timeWeightedInvestmentValuesWithCurrencyEffect
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
positions.push({
|
|
|
|
|
feeInBaseCurrency,
|
|
|
|
|
timeWeightedInvestment,
|
|
|
|
@ -374,11 +391,11 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
netPerformancePercentage: !hasErrors
|
|
|
|
|
? (netPerformancePercentage ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
netPerformancePercentageWithCurrencyEffect: !hasErrors
|
|
|
|
|
? (netPerformancePercentageWithCurrencyEffect ?? null)
|
|
|
|
|
netPerformancePercentageWithCurrencyEffectMap: !hasErrors
|
|
|
|
|
? (netPerformancePercentageWithCurrencyEffectMap ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
netPerformanceWithCurrencyEffect: !hasErrors
|
|
|
|
|
? (netPerformanceWithCurrencyEffect ?? null)
|
|
|
|
|
netPerformanceWithCurrencyEffectMap: !hasErrors
|
|
|
|
|
? (netPerformanceWithCurrencyEffectMap ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
symbol: item.symbol,
|
|
|
|
@ -411,205 +428,9 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const overall = this.calculateOverallPerformance(positions);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...overall,
|
|
|
|
|
errors,
|
|
|
|
|
positions,
|
|
|
|
|
totalInterestWithCurrencyEffect,
|
|
|
|
|
totalLiabilitiesWithCurrencyEffect,
|
|
|
|
|
totalValuablesWithCurrencyEffect,
|
|
|
|
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getChart({
|
|
|
|
|
dateRange = 'max',
|
|
|
|
|
withDataDecimation = true
|
|
|
|
|
}: {
|
|
|
|
|
dateRange?: DateRange;
|
|
|
|
|
withDataDecimation?: boolean;
|
|
|
|
|
}): Promise<HistoricalDataItem[]> {
|
|
|
|
|
const { endDate, startDate } = getIntervalFromDateRange(
|
|
|
|
|
dateRange,
|
|
|
|
|
this.getStartDate()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const daysInMarket = differenceInDays(endDate, startDate) + 1;
|
|
|
|
|
const step = withDataDecimation
|
|
|
|
|
? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS))
|
|
|
|
|
: 1;
|
|
|
|
|
|
|
|
|
|
return this.getChartData({
|
|
|
|
|
step,
|
|
|
|
|
end: endDate,
|
|
|
|
|
start: startDate
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getChartData({
|
|
|
|
|
end = new Date(Date.now()),
|
|
|
|
|
start,
|
|
|
|
|
step = 1
|
|
|
|
|
}: {
|
|
|
|
|
end?: Date;
|
|
|
|
|
start: Date;
|
|
|
|
|
step?: number;
|
|
|
|
|
}): Promise<HistoricalDataItem[]> {
|
|
|
|
|
const symbols: { [symbol: string]: boolean } = {};
|
|
|
|
|
|
|
|
|
|
const transactionPointsBeforeEndDate =
|
|
|
|
|
this.transactionPoints?.filter((transactionPoint) => {
|
|
|
|
|
return isBefore(parseDate(transactionPoint.date), end);
|
|
|
|
|
}) ?? [];
|
|
|
|
|
|
|
|
|
|
const currencies: { [symbol: string]: string } = {};
|
|
|
|
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
|
|
|
|
const firstIndex = transactionPointsBeforeEndDate.length;
|
|
|
|
|
|
|
|
|
|
let dates = eachDayOfInterval({ start, end }, { step }).map((date) => {
|
|
|
|
|
return resetHours(date);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const includesEndDate = isSameDay(last(dates), end);
|
|
|
|
|
|
|
|
|
|
if (!includesEndDate) {
|
|
|
|
|
dates.push(resetHours(end));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (transactionPointsBeforeEndDate.length > 0) {
|
|
|
|
|
for (const {
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol
|
|
|
|
|
} of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
|
|
|
|
dataGatheringItems.push({
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol
|
|
|
|
|
});
|
|
|
|
|
currencies[symbol] = currency;
|
|
|
|
|
symbols[symbol] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { dataProviderInfos, values: marketSymbols } =
|
|
|
|
|
await this.currentRateService.getValues({
|
|
|
|
|
dataGatheringItems,
|
|
|
|
|
dateQuery: {
|
|
|
|
|
in: dates
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.dataProviderInfos = dataProviderInfos;
|
|
|
|
|
|
|
|
|
|
const marketSymbolMap: {
|
|
|
|
|
[date: string]: { [symbol: string]: Big };
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
let exchangeRatesByCurrency =
|
|
|
|
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
|
|
|
|
currencies: uniq(Object.values(currencies)),
|
|
|
|
|
endDate: endOfDay(end),
|
|
|
|
|
startDate: this.getStartDate(),
|
|
|
|
|
targetCurrency: this.currency
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const marketSymbol of marketSymbols) {
|
|
|
|
|
const dateString = format(marketSymbol.date, DATE_FORMAT);
|
|
|
|
|
if (!marketSymbolMap[dateString]) {
|
|
|
|
|
marketSymbolMap[dateString] = {};
|
|
|
|
|
}
|
|
|
|
|
if (marketSymbol.marketPrice) {
|
|
|
|
|
marketSymbolMap[dateString][marketSymbol.symbol] = new Big(
|
|
|
|
|
marketSymbol.marketPrice
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accumulatedValuesByDate: {
|
|
|
|
|
[date: string]: {
|
|
|
|
|
investmentValueWithCurrencyEffect: Big;
|
|
|
|
|
totalCurrentValue: Big;
|
|
|
|
|
totalCurrentValueWithCurrencyEffect: Big;
|
|
|
|
|
totalAccountBalanceWithCurrencyEffect: 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 };
|
|
|
|
|
investmentValuesAccumulated: { [date: string]: Big };
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect: { [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,
|
|
|
|
|
investmentValuesAccumulated,
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect,
|
|
|
|
|
investmentValuesWithCurrencyEffect,
|
|
|
|
|
netPerformanceValues,
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect,
|
|
|
|
|
timeWeightedInvestmentValues,
|
|
|
|
|
timeWeightedInvestmentValuesWithCurrencyEffect
|
|
|
|
|
} = this.getSymbolMetrics({
|
|
|
|
|
end,
|
|
|
|
|
marketSymbolMap,
|
|
|
|
|
start,
|
|
|
|
|
step,
|
|
|
|
|
symbol,
|
|
|
|
|
dataSource: null,
|
|
|
|
|
exchangeRates:
|
|
|
|
|
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`],
|
|
|
|
|
isChartMode: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
valuesBySymbol[symbol] = {
|
|
|
|
|
currentValues,
|
|
|
|
|
currentValuesWithCurrencyEffect,
|
|
|
|
|
investmentValuesAccumulated,
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect,
|
|
|
|
|
investmentValuesWithCurrencyEffect,
|
|
|
|
|
netPerformanceValues,
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect,
|
|
|
|
|
timeWeightedInvestmentValues,
|
|
|
|
|
timeWeightedInvestmentValuesWithCurrencyEffect
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let lastDate = format(this.startDate, DATE_FORMAT);
|
|
|
|
|
|
|
|
|
|
for (const currentDate of dates) {
|
|
|
|
|
const dateString = format(currentDate, DATE_FORMAT);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString] = {
|
|
|
|
|
investmentValueWithCurrencyEffect: new Big(0),
|
|
|
|
|
totalAccountBalanceWithCurrencyEffect: new Big(0),
|
|
|
|
|
totalCurrentValue: new Big(0),
|
|
|
|
|
totalCurrentValueWithCurrencyEffect: new Big(0),
|
|
|
|
|
totalInvestmentValue: new Big(0),
|
|
|
|
|
totalInvestmentValueWithCurrencyEffect: new Big(0),
|
|
|
|
|
totalNetPerformanceValue: new Big(0),
|
|
|
|
|
totalNetPerformanceValueWithCurrencyEffect: new Big(0),
|
|
|
|
|
totalTimeWeightedInvestmentValue: new Big(0),
|
|
|
|
|
totalTimeWeightedInvestmentValueWithCurrencyEffect: new Big(0)
|
|
|
|
|
};
|
|
|
|
|
let lastDate = chartDates[0];
|
|
|
|
|
|
|
|
|
|
for (const dateString of chartDates) {
|
|
|
|
|
for (const symbol of Object.keys(valuesBySymbol)) {
|
|
|
|
|
const symbolValues = valuesBySymbol[symbol];
|
|
|
|
|
|
|
|
|
@ -647,91 +468,63 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
dateString
|
|
|
|
|
] ?? new Big(0);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].investmentValueWithCurrencyEffect =
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].investmentValueWithCurrencyEffect.add(
|
|
|
|
|
investmentValueWithCurrencyEffect
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalCurrentValue =
|
|
|
|
|
accumulatedValuesByDate[dateString].totalCurrentValue.add(
|
|
|
|
|
currentValue
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalCurrentValueWithCurrencyEffect = accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalCurrentValueWithCurrencyEffect.add(
|
|
|
|
|
currentValueWithCurrencyEffect
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalInvestmentValue =
|
|
|
|
|
accumulatedValuesByDate[dateString].totalInvestmentValue.add(
|
|
|
|
|
investmentValueAccumulated
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalInvestmentValueWithCurrencyEffect = accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalInvestmentValueWithCurrencyEffect.add(
|
|
|
|
|
investmentValueAccumulatedWithCurrencyEffect
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalNetPerformanceValue =
|
|
|
|
|
accumulatedValuesByDate[dateString].totalNetPerformanceValue.add(
|
|
|
|
|
netPerformanceValue
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalNetPerformanceValueWithCurrencyEffect = accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalNetPerformanceValueWithCurrencyEffect.add(
|
|
|
|
|
netPerformanceValueWithCurrencyEffect
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalTimeWeightedInvestmentValue =
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalTimeWeightedInvestmentValue.add(timeWeightedInvestmentValue);
|
|
|
|
|
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalTimeWeightedInvestmentValueWithCurrencyEffect =
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalTimeWeightedInvestmentValueWithCurrencyEffect.add(
|
|
|
|
|
timeWeightedInvestmentValueWithCurrencyEffect
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.accountBalanceItems.some(({ date }) => {
|
|
|
|
|
return date === dateString;
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalAccountBalanceWithCurrencyEffect = new Big(
|
|
|
|
|
this.accountBalanceItems.find(({ date }) => {
|
|
|
|
|
return date === dateString;
|
|
|
|
|
}).value
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
accumulatedValuesByDate[
|
|
|
|
|
dateString
|
|
|
|
|
].totalAccountBalanceWithCurrencyEffect =
|
|
|
|
|
accumulatedValuesByDate[lastDate]
|
|
|
|
|
?.totalAccountBalanceWithCurrencyEffect ?? new Big(0);
|
|
|
|
|
accumulatedValuesByDate[dateString] = {
|
|
|
|
|
investmentValueWithCurrencyEffect: (
|
|
|
|
|
accumulatedValuesByDate[dateString]
|
|
|
|
|
?.investmentValueWithCurrencyEffect ?? new Big(0)
|
|
|
|
|
).add(investmentValueWithCurrencyEffect),
|
|
|
|
|
totalAccountBalanceWithCurrencyEffect: this.accountBalanceItems.some(
|
|
|
|
|
({ date }) => {
|
|
|
|
|
return date === dateString;
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
? new Big(
|
|
|
|
|
this.accountBalanceItems.find(({ date }) => {
|
|
|
|
|
return date === dateString;
|
|
|
|
|
}).value
|
|
|
|
|
)
|
|
|
|
|
: (accumulatedValuesByDate[lastDate]
|
|
|
|
|
?.totalAccountBalanceWithCurrencyEffect ?? new Big(0)),
|
|
|
|
|
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(investmentValueAccumulated),
|
|
|
|
|
totalInvestmentValueWithCurrencyEffect: (
|
|
|
|
|
accumulatedValuesByDate[dateString]
|
|
|
|
|
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0)
|
|
|
|
|
).add(investmentValueAccumulatedWithCurrencyEffect),
|
|
|
|
|
totalNetPerformanceValue: (
|
|
|
|
|
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
|
|
|
|
|
new Big(0)
|
|
|
|
|
).add(netPerformanceValue),
|
|
|
|
|
totalNetPerformanceValueWithCurrencyEffect: (
|
|
|
|
|
accumulatedValuesByDate[dateString]
|
|
|
|
|
?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0)
|
|
|
|
|
).add(netPerformanceValueWithCurrencyEffect),
|
|
|
|
|
totalTimeWeightedInvestmentValue: (
|
|
|
|
|
accumulatedValuesByDate[dateString]
|
|
|
|
|
?.totalTimeWeightedInvestmentValue ?? new Big(0)
|
|
|
|
|
).add(timeWeightedInvestmentValue),
|
|
|
|
|
totalTimeWeightedInvestmentValueWithCurrencyEffect: (
|
|
|
|
|
accumulatedValuesByDate[dateString]
|
|
|
|
|
?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0)
|
|
|
|
|
).add(timeWeightedInvestmentValueWithCurrencyEffect)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastDate = dateString;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
|
|
|
|
|
const historicalData: HistoricalDataItem[] = Object.entries(
|
|
|
|
|
accumulatedValuesByDate
|
|
|
|
|
).map(([date, values]) => {
|
|
|
|
|
const {
|
|
|
|
|
investmentValueWithCurrencyEffect,
|
|
|
|
|
totalAccountBalanceWithCurrencyEffect,
|
|
|
|
@ -749,7 +542,6 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
? 0
|
|
|
|
|
: totalNetPerformanceValue
|
|
|
|
|
.div(totalTimeWeightedInvestmentValue)
|
|
|
|
|
.mul(100)
|
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
|
|
const netPerformanceInPercentageWithCurrencyEffect =
|
|
|
|
@ -757,7 +549,6 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
? 0
|
|
|
|
|
: totalNetPerformanceValueWithCurrencyEffect
|
|
|
|
|
.div(totalTimeWeightedInvestmentValueWithCurrencyEffect)
|
|
|
|
|
.mul(100)
|
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
@ -781,6 +572,19 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber()
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const overall = this.calculateOverallPerformance(positions);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...overall,
|
|
|
|
|
errors,
|
|
|
|
|
historicalData,
|
|
|
|
|
positions,
|
|
|
|
|
totalInterestWithCurrencyEffect,
|
|
|
|
|
totalLiabilitiesWithCurrencyEffect,
|
|
|
|
|
totalValuablesWithCurrencyEffect,
|
|
|
|
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getDataProviderInfos() {
|
|
|
|
@ -861,6 +665,70 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
return this.snapshot;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getPerformance({ end, start }) {
|
|
|
|
|
await this.snapshotPromise;
|
|
|
|
|
|
|
|
|
|
const { historicalData } = this.snapshot;
|
|
|
|
|
|
|
|
|
|
const chart: HistoricalDataItem[] = [];
|
|
|
|
|
|
|
|
|
|
let netPerformanceAtStartDate: number;
|
|
|
|
|
let netPerformanceWithCurrencyEffectAtStartDate: number;
|
|
|
|
|
let totalInvestmentValuesWithCurrencyEffect: number[] = [];
|
|
|
|
|
|
|
|
|
|
for (let historicalDataItem of historicalData) {
|
|
|
|
|
const date = resetHours(parseDate(historicalDataItem.date));
|
|
|
|
|
|
|
|
|
|
if (!isBefore(date, start) && !isAfter(date, end)) {
|
|
|
|
|
if (!isNumber(netPerformanceAtStartDate)) {
|
|
|
|
|
netPerformanceAtStartDate = historicalDataItem.netPerformance;
|
|
|
|
|
|
|
|
|
|
netPerformanceWithCurrencyEffectAtStartDate =
|
|
|
|
|
historicalDataItem.netPerformanceWithCurrencyEffect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const netPerformanceSinceStartDate =
|
|
|
|
|
historicalDataItem.netPerformance - netPerformanceAtStartDate;
|
|
|
|
|
|
|
|
|
|
const netPerformanceWithCurrencyEffectSinceStartDate =
|
|
|
|
|
historicalDataItem.netPerformanceWithCurrencyEffect -
|
|
|
|
|
netPerformanceWithCurrencyEffectAtStartDate;
|
|
|
|
|
|
|
|
|
|
if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) {
|
|
|
|
|
totalInvestmentValuesWithCurrencyEffect.push(
|
|
|
|
|
historicalDataItem.totalInvestmentValueWithCurrencyEffect
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timeWeightedInvestmentValue =
|
|
|
|
|
totalInvestmentValuesWithCurrencyEffect.length > 0
|
|
|
|
|
? sum(totalInvestmentValuesWithCurrencyEffect) /
|
|
|
|
|
totalInvestmentValuesWithCurrencyEffect.length
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
chart.push({
|
|
|
|
|
...historicalDataItem,
|
|
|
|
|
netPerformance:
|
|
|
|
|
historicalDataItem.netPerformance - netPerformanceAtStartDate,
|
|
|
|
|
netPerformanceWithCurrencyEffect:
|
|
|
|
|
netPerformanceWithCurrencyEffectSinceStartDate,
|
|
|
|
|
netPerformanceInPercentage:
|
|
|
|
|
netPerformanceSinceStartDate / timeWeightedInvestmentValue,
|
|
|
|
|
netPerformanceInPercentageWithCurrencyEffect:
|
|
|
|
|
netPerformanceWithCurrencyEffectSinceStartDate /
|
|
|
|
|
timeWeightedInvestmentValue,
|
|
|
|
|
// TODO: Add net worth with valuables
|
|
|
|
|
// netWorth: totalCurrentValueWithCurrencyEffect
|
|
|
|
|
// .plus(totalAccountBalanceWithCurrencyEffect)
|
|
|
|
|
// .toNumber()
|
|
|
|
|
netWorth: 0
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { chart };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getStartDate() {
|
|
|
|
|
let firstAccountBalanceDate: Date;
|
|
|
|
|
let firstActivityDate: Date;
|
|
|
|
@ -889,23 +757,21 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract getSymbolMetrics({
|
|
|
|
|
chartDateMap,
|
|
|
|
|
dataSource,
|
|
|
|
|
end,
|
|
|
|
|
exchangeRates,
|
|
|
|
|
isChartMode,
|
|
|
|
|
marketSymbolMap,
|
|
|
|
|
start,
|
|
|
|
|
step,
|
|
|
|
|
symbol
|
|
|
|
|
}: {
|
|
|
|
|
chartDateMap: { [date: string]: boolean };
|
|
|
|
|
end: Date;
|
|
|
|
|
exchangeRates: { [dateString: string]: number };
|
|
|
|
|
isChartMode?: boolean;
|
|
|
|
|
marketSymbolMap: {
|
|
|
|
|
[date: string]: { [symbol: string]: Big };
|
|
|
|
|
};
|
|
|
|
|
start: Date;
|
|
|
|
|
step?: number;
|
|
|
|
|
} & AssetProfileIdentifier): SymbolMetrics;
|
|
|
|
|
|
|
|
|
|
public getTransactionPoints() {
|
|
|
|
@ -918,6 +784,66 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
return this.snapshot.totalValuablesWithCurrencyEffect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getChartDateMap({
|
|
|
|
|
endDate,
|
|
|
|
|
startDate,
|
|
|
|
|
step
|
|
|
|
|
}: {
|
|
|
|
|
endDate: Date;
|
|
|
|
|
startDate: Date;
|
|
|
|
|
step: number;
|
|
|
|
|
}) {
|
|
|
|
|
// Create a map of all relevant chart dates:
|
|
|
|
|
// 1. Add transaction point dates
|
|
|
|
|
let chartDateMap = this.transactionPoints.reduce((result, { date }) => {
|
|
|
|
|
result[date] = true;
|
|
|
|
|
return result;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
// 2. Add dates between transactions respecting the specified step size
|
|
|
|
|
for (let date of eachDayOfInterval(
|
|
|
|
|
{ end: endDate, start: startDate },
|
|
|
|
|
{ step }
|
|
|
|
|
)) {
|
|
|
|
|
chartDateMap[format(date, DATE_FORMAT)] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (step > 1) {
|
|
|
|
|
// Reduce the step size of recent dates
|
|
|
|
|
for (let date of eachDayOfInterval(
|
|
|
|
|
{ end: endDate, start: subDays(endDate, 90) },
|
|
|
|
|
{ step: 1 }
|
|
|
|
|
)) {
|
|
|
|
|
chartDateMap[format(date, DATE_FORMAT)] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure the end date is present
|
|
|
|
|
chartDateMap[format(endDate, DATE_FORMAT)] = true;
|
|
|
|
|
|
|
|
|
|
// Make sure some key dates are present
|
|
|
|
|
for (let dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
|
|
|
|
|
const { endDate: dateRangeEnd, startDate: dateRangeStart } =
|
|
|
|
|
getIntervalFromDateRange(dateRange);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!isBefore(dateRangeStart, startDate) &&
|
|
|
|
|
!isAfter(dateRangeStart, endDate)
|
|
|
|
|
) {
|
|
|
|
|
chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!isBefore(dateRangeEnd, startDate) &&
|
|
|
|
|
!isAfter(dateRangeEnd, endDate)
|
|
|
|
|
) {
|
|
|
|
|
chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chartDateMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private computeTransactionPoints() {
|
|
|
|
|
this.transactionPoints = [];
|
|
|
|
|
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
|
|
|
|
@ -1057,52 +983,47 @@ export abstract class PortfolioCalculator {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async initialize() {
|
|
|
|
|
if (this.useCache) {
|
|
|
|
|
const startTimeTotal = performance.now();
|
|
|
|
|
const startTimeTotal = performance.now();
|
|
|
|
|
|
|
|
|
|
const cachedSnapshot = await this.redisCacheService.get(
|
|
|
|
|
this.redisCacheService.getPortfolioSnapshotKey({
|
|
|
|
|
userId: this.userId
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
const cachedSnapshot = await this.redisCacheService.get(
|
|
|
|
|
this.redisCacheService.getPortfolioSnapshotKey({
|
|
|
|
|
filters: this.filters,
|
|
|
|
|
userId: this.userId
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (cachedSnapshot) {
|
|
|
|
|
this.snapshot = plainToClass(
|
|
|
|
|
PortfolioSnapshot,
|
|
|
|
|
JSON.parse(cachedSnapshot)
|
|
|
|
|
);
|
|
|
|
|
if (cachedSnapshot) {
|
|
|
|
|
this.snapshot = plainToClass(
|
|
|
|
|
PortfolioSnapshot,
|
|
|
|
|
JSON.parse(cachedSnapshot)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Logger.debug(
|
|
|
|
|
`Fetched portfolio snapshot from cache in ${(
|
|
|
|
|
(performance.now() - startTimeTotal) /
|
|
|
|
|
1000
|
|
|
|
|
).toFixed(3)} seconds`,
|
|
|
|
|
'PortfolioCalculator'
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
this.snapshot = await this.computeSnapshot(
|
|
|
|
|
this.startDate,
|
|
|
|
|
this.endDate
|
|
|
|
|
);
|
|
|
|
|
Logger.debug(
|
|
|
|
|
`Fetched portfolio snapshot from cache in ${(
|
|
|
|
|
(performance.now() - startTimeTotal) /
|
|
|
|
|
1000
|
|
|
|
|
).toFixed(3)} seconds`,
|
|
|
|
|
'PortfolioCalculator'
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
this.snapshot = await this.computeSnapshot();
|
|
|
|
|
|
|
|
|
|
this.redisCacheService.set(
|
|
|
|
|
this.redisCacheService.getPortfolioSnapshotKey({
|
|
|
|
|
userId: this.userId
|
|
|
|
|
}),
|
|
|
|
|
JSON.stringify(this.snapshot),
|
|
|
|
|
this.configurationService.get('CACHE_QUOTES_TTL')
|
|
|
|
|
);
|
|
|
|
|
this.redisCacheService.set(
|
|
|
|
|
this.redisCacheService.getPortfolioSnapshotKey({
|
|
|
|
|
filters: this.filters,
|
|
|
|
|
userId: this.userId
|
|
|
|
|
}),
|
|
|
|
|
JSON.stringify(this.snapshot),
|
|
|
|
|
this.configurationService.get('CACHE_QUOTES_TTL')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Logger.debug(
|
|
|
|
|
`Computed portfolio snapshot in ${(
|
|
|
|
|
(performance.now() - startTimeTotal) /
|
|
|
|
|
1000
|
|
|
|
|
).toFixed(3)} seconds`,
|
|
|
|
|
'PortfolioCalculator'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate);
|
|
|
|
|
Logger.debug(
|
|
|
|
|
`Computed portfolio snapshot in ${(
|
|
|
|
|
(performance.now() - startTimeTotal) /
|
|
|
|
|
1000
|
|
|
|
|
).toFixed(3)} seconds`,
|
|
|
|
|
'PortfolioCalculator'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|