Feature/introduce portfolio calculator factory (#3214)
* Introduce portfolio calculator factory * Update changelog
This commit is contained in:
parent
0c68474802
commit
8cd6c34ed8
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved the usability of the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental)
|
- Improved the usability of the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental)
|
||||||
|
- Introduced a factory for the portfolio calculations to support different algorithms in future
|
||||||
|
|
||||||
## 2.69.0 - 2024-03-30
|
## 2.69.0 - 2024-03-30
|
||||||
|
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||||
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
|
import {
|
||||||
|
SymbolMetrics,
|
||||||
|
TimelinePosition,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export class MWRPortfolioCalculator extends PortfolioCalculator {
|
||||||
|
protected calculateOverallPerformance(
|
||||||
|
positions: TimelinePosition[]
|
||||||
|
): CurrentPositions {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getSymbolMetrics({
|
||||||
|
dataSource,
|
||||||
|
end,
|
||||||
|
exchangeRates,
|
||||||
|
isChartMode = false,
|
||||||
|
marketSymbolMap,
|
||||||
|
start,
|
||||||
|
step = 1,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
end: Date;
|
||||||
|
exchangeRates: { [dateString: string]: number };
|
||||||
|
isChartMode?: boolean;
|
||||||
|
marketSymbolMap: {
|
||||||
|
[date: string]: { [symbol: string]: Big };
|
||||||
|
};
|
||||||
|
start: Date;
|
||||||
|
step?: number;
|
||||||
|
} & UniqueAsset): SymbolMetrics {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
export const activityDummyData = {
|
||||||
|
accountId: undefined,
|
||||||
|
accountUserId: undefined,
|
||||||
|
comment: undefined,
|
||||||
|
createdAt: new Date(),
|
||||||
|
feeInBaseCurrency: undefined,
|
||||||
|
id: undefined,
|
||||||
|
isDraft: false,
|
||||||
|
symbolProfileId: undefined,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
userId: undefined,
|
||||||
|
value: undefined,
|
||||||
|
valueInBaseCurrency: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
export const symbolProfileDummyData = {
|
||||||
|
activitiesCount: undefined,
|
||||||
|
assetClass: undefined,
|
||||||
|
assetSubClass: undefined,
|
||||||
|
countries: [],
|
||||||
|
createdAt: undefined,
|
||||||
|
id: undefined,
|
||||||
|
sectors: [],
|
||||||
|
updatedAt: undefined
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MWRPortfolioCalculator } from './mwr/portfolio-calculator';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
import { TWRPortfolioCalculator } from './twr/portfolio-calculator';
|
||||||
|
|
||||||
|
export enum PerformanceCalculationType {
|
||||||
|
MWR = 'MWR', // Money-Weighted Rate of Return
|
||||||
|
TWR = 'TWR' // Time-Weighted Rate of Return
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PortfolioCalculatorFactory {
|
||||||
|
public constructor(
|
||||||
|
private readonly currentRateService: CurrentRateService,
|
||||||
|
private readonly exchangeRateDataService: ExchangeRateDataService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType,
|
||||||
|
currency
|
||||||
|
}: {
|
||||||
|
activities: Activity[];
|
||||||
|
calculationType: PerformanceCalculationType;
|
||||||
|
currency: string;
|
||||||
|
}): PortfolioCalculator {
|
||||||
|
switch (calculationType) {
|
||||||
|
case PerformanceCalculationType.MWR:
|
||||||
|
return new MWRPortfolioCalculator({
|
||||||
|
activities,
|
||||||
|
currency,
|
||||||
|
currentRateService: this.currentRateService,
|
||||||
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
|
});
|
||||||
|
case PerformanceCalculationType.TWR:
|
||||||
|
return new TWRPortfolioCalculator({
|
||||||
|
activities,
|
||||||
|
currency,
|
||||||
|
currentRateService: this.currentRateService,
|
||||||
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid calculation type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
788
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
Normal file
788
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
Normal file
@ -0,0 +1,788 @@
|
|||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
|
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
|
||||||
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
|
import {
|
||||||
|
DataProviderInfo,
|
||||||
|
HistoricalDataItem,
|
||||||
|
InvestmentItem,
|
||||||
|
ResponseError,
|
||||||
|
SymbolMetrics,
|
||||||
|
TimelinePosition,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { GroupBy } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
import { Big } from 'big.js';
|
||||||
|
import {
|
||||||
|
eachDayOfInterval,
|
||||||
|
endOfDay,
|
||||||
|
format,
|
||||||
|
isBefore,
|
||||||
|
isSameDay,
|
||||||
|
max,
|
||||||
|
subDays
|
||||||
|
} from 'date-fns';
|
||||||
|
import { isNumber, last, uniq } from 'lodash';
|
||||||
|
|
||||||
|
export abstract class PortfolioCalculator {
|
||||||
|
protected static readonly ENABLE_LOGGING = false;
|
||||||
|
|
||||||
|
protected orders: PortfolioOrder[];
|
||||||
|
|
||||||
|
private currency: string;
|
||||||
|
private currentRateService: CurrentRateService;
|
||||||
|
private dataProviderInfos: DataProviderInfo[];
|
||||||
|
private exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
private transactionPoints: TransactionPoint[];
|
||||||
|
|
||||||
|
public constructor({
|
||||||
|
activities,
|
||||||
|
currency,
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
}: {
|
||||||
|
activities: Activity[];
|
||||||
|
currency: string;
|
||||||
|
currentRateService: CurrentRateService;
|
||||||
|
exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
}) {
|
||||||
|
this.currency = currency;
|
||||||
|
this.currentRateService = currentRateService;
|
||||||
|
this.exchangeRateDataService = exchangeRateDataService;
|
||||||
|
this.orders = activities.map(
|
||||||
|
({ date, fee, quantity, SymbolProfile, type, unitPrice }) => {
|
||||||
|
return {
|
||||||
|
SymbolProfile,
|
||||||
|
type,
|
||||||
|
date: format(date, DATE_FORMAT),
|
||||||
|
fee: new Big(fee),
|
||||||
|
quantity: new Big(quantity),
|
||||||
|
unitPrice: new Big(unitPrice)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.orders.sort((a, b) => {
|
||||||
|
return a.date?.localeCompare(b.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.computeTransactionPoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract calculateOverallPerformance(
|
||||||
|
positions: TimelinePosition[]
|
||||||
|
): CurrentPositions;
|
||||||
|
|
||||||
|
public getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket,
|
||||||
|
netPerformancePercent
|
||||||
|
}: {
|
||||||
|
daysInMarket: number;
|
||||||
|
netPerformancePercent: Big;
|
||||||
|
}): Big {
|
||||||
|
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||||
|
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||||
|
return new Big(
|
||||||
|
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
||||||
|
).minus(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Big(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const currentDate of dates) {
|
||||||
|
const dateString = format(currentDate, DATE_FORMAT);
|
||||||
|
|
||||||
|
for (const symbol of Object.keys(valuesBySymbol)) {
|
||||||
|
const symbolValues = valuesBySymbol[symbol];
|
||||||
|
|
||||||
|
const currentValue =
|
||||||
|
symbolValues.currentValues?.[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
const currentValueWithCurrencyEffect =
|
||||||
|
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ??
|
||||||
|
new Big(0);
|
||||||
|
|
||||||
|
const investmentValueAccumulated =
|
||||||
|
symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
const investmentValueAccumulatedWithCurrencyEffect =
|
||||||
|
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[
|
||||||
|
dateString
|
||||||
|
] ?? new Big(0);
|
||||||
|
|
||||||
|
const investmentValueWithCurrencyEffect =
|
||||||
|
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ??
|
||||||
|
new Big(0);
|
||||||
|
|
||||||
|
const netPerformanceValue =
|
||||||
|
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
const netPerformanceValueWithCurrencyEffect =
|
||||||
|
symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ??
|
||||||
|
new Big(0);
|
||||||
|
|
||||||
|
const timeWeightedInvestmentValue =
|
||||||
|
symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
const timeWeightedInvestmentValueWithCurrencyEffect =
|
||||||
|
symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[
|
||||||
|
dateString
|
||||||
|
] ?? new Big(0);
|
||||||
|
|
||||||
|
accumulatedValuesByDate[dateString] = {
|
||||||
|
investmentValueWithCurrencyEffect: (
|
||||||
|
accumulatedValuesByDate[dateString]
|
||||||
|
?.investmentValueWithCurrencyEffect ?? new Big(0)
|
||||||
|
).add(investmentValueWithCurrencyEffect),
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
|
||||||
|
const {
|
||||||
|
investmentValueWithCurrencyEffect,
|
||||||
|
totalCurrentValue,
|
||||||
|
totalCurrentValueWithCurrencyEffect,
|
||||||
|
totalInvestmentValue,
|
||||||
|
totalInvestmentValueWithCurrencyEffect,
|
||||||
|
totalNetPerformanceValue,
|
||||||
|
totalNetPerformanceValueWithCurrencyEffect,
|
||||||
|
totalTimeWeightedInvestmentValue,
|
||||||
|
totalTimeWeightedInvestmentValueWithCurrencyEffect
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0)
|
||||||
|
? 0
|
||||||
|
: totalNetPerformanceValue
|
||||||
|
.div(totalTimeWeightedInvestmentValue)
|
||||||
|
.mul(100)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
const netPerformanceInPercentageWithCurrencyEffect =
|
||||||
|
totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0)
|
||||||
|
? 0
|
||||||
|
: totalNetPerformanceValueWithCurrencyEffect
|
||||||
|
.div(totalTimeWeightedInvestmentValueWithCurrencyEffect)
|
||||||
|
.mul(100)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
netPerformanceInPercentage,
|
||||||
|
netPerformanceInPercentageWithCurrencyEffect,
|
||||||
|
investmentValueWithCurrencyEffect:
|
||||||
|
investmentValueWithCurrencyEffect.toNumber(),
|
||||||
|
netPerformance: totalNetPerformanceValue.toNumber(),
|
||||||
|
netPerformanceWithCurrencyEffect:
|
||||||
|
totalNetPerformanceValueWithCurrencyEffect.toNumber(),
|
||||||
|
totalInvestment: totalInvestmentValue.toNumber(),
|
||||||
|
totalInvestmentValueWithCurrencyEffect:
|
||||||
|
totalInvestmentValueWithCurrencyEffect.toNumber(),
|
||||||
|
value: totalCurrentValue.toNumber(),
|
||||||
|
valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCurrentPositions(
|
||||||
|
start: Date,
|
||||||
|
end?: Date
|
||||||
|
): Promise<CurrentPositions> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
positions: [],
|
||||||
|
totalInvestment: new Big(0),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currencies: { [symbol: string]: string } = {};
|
||||||
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
|
let dates: Date[] = [];
|
||||||
|
let firstIndex = transactionPoints.length;
|
||||||
|
let firstTransactionPoint: TransactionPoint = null;
|
||||||
|
|
||||||
|
dates.push(resetHours(start));
|
||||||
|
|
||||||
|
for (const { currency, dataSource, symbol } of transactionPoints[
|
||||||
|
firstIndex - 1
|
||||||
|
].items) {
|
||||||
|
dataGatheringItems.push({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
|
||||||
|
currencies[symbol] = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < transactionPoints.length; i++) {
|
||||||
|
if (
|
||||||
|
!isBefore(parseDate(transactionPoints[i].date), start) &&
|
||||||
|
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(),
|
||||||
|
targetCurrency: this.currency
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
dataProviderInfos,
|
||||||
|
errors: currentRateErrors,
|
||||||
|
values: marketSymbols
|
||||||
|
} = await this.currentRateService.getValues({
|
||||||
|
dataGatheringItems,
|
||||||
|
dateQuery: {
|
||||||
|
in: dates
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataProviderInfos = dataProviderInfos;
|
||||||
|
|
||||||
|
const marketSymbolMap: {
|
||||||
|
[date: string]: { [symbol: string]: Big };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
for (const marketSymbol of marketSymbols) {
|
||||||
|
const date = format(marketSymbol.date, DATE_FORMAT);
|
||||||
|
|
||||||
|
if (!marketSymbolMap[date]) {
|
||||||
|
marketSymbolMap[date] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marketSymbol.marketPrice) {
|
||||||
|
marketSymbolMap[date][marketSymbol.symbol] = new Big(
|
||||||
|
marketSymbol.marketPrice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endDateString = format(endDate, DATE_FORMAT);
|
||||||
|
|
||||||
|
if (firstIndex > 0) {
|
||||||
|
firstIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions: TimelinePosition[] = [];
|
||||||
|
let hasAnySymbolMetricsErrors = false;
|
||||||
|
|
||||||
|
const errors: ResponseError['errors'] = [];
|
||||||
|
|
||||||
|
for (const item of lastTransactionPoint.items) {
|
||||||
|
const marketPriceInBaseCurrency = (
|
||||||
|
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
|
||||||
|
).mul(
|
||||||
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
||||||
|
endDateString
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
grossPerformance,
|
||||||
|
grossPerformancePercentage,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
|
grossPerformanceWithCurrencyEffect,
|
||||||
|
hasErrors,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercentage,
|
||||||
|
netPerformancePercentageWithCurrencyEffect,
|
||||||
|
netPerformanceWithCurrencyEffect,
|
||||||
|
timeWeightedInvestment,
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect,
|
||||||
|
totalDividend,
|
||||||
|
totalDividendInBaseCurrency,
|
||||||
|
totalInvestment,
|
||||||
|
totalInvestmentWithCurrencyEffect
|
||||||
|
} = this.getSymbolMetrics({
|
||||||
|
marketSymbolMap,
|
||||||
|
start,
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
end: endDate,
|
||||||
|
exchangeRates:
|
||||||
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
|
|
||||||
|
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
||||||
|
|
||||||
|
positions.push({
|
||||||
|
dividend: totalDividend,
|
||||||
|
dividendInBaseCurrency: totalDividendInBaseCurrency,
|
||||||
|
timeWeightedInvestment,
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect,
|
||||||
|
averagePrice: item.averagePrice,
|
||||||
|
currency: item.currency,
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
fee: item.fee,
|
||||||
|
firstBuyDate: item.firstBuyDate,
|
||||||
|
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
|
||||||
|
grossPerformancePercentage: !hasErrors
|
||||||
|
? grossPerformancePercentage ?? null
|
||||||
|
: null,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: !hasErrors
|
||||||
|
? grossPerformancePercentageWithCurrencyEffect ?? null
|
||||||
|
: null,
|
||||||
|
grossPerformanceWithCurrencyEffect: !hasErrors
|
||||||
|
? grossPerformanceWithCurrencyEffect ?? null
|
||||||
|
: null,
|
||||||
|
investment: totalInvestment,
|
||||||
|
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
|
||||||
|
marketPrice:
|
||||||
|
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
|
||||||
|
marketPriceInBaseCurrency:
|
||||||
|
marketPriceInBaseCurrency?.toNumber() ?? null,
|
||||||
|
netPerformance: !hasErrors ? netPerformance ?? null : null,
|
||||||
|
netPerformancePercentage: !hasErrors
|
||||||
|
? netPerformancePercentage ?? null
|
||||||
|
: null,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: !hasErrors
|
||||||
|
? netPerformancePercentageWithCurrencyEffect ?? null
|
||||||
|
: null,
|
||||||
|
netPerformanceWithCurrencyEffect: !hasErrors
|
||||||
|
? netPerformanceWithCurrencyEffect ?? null
|
||||||
|
: null,
|
||||||
|
quantity: item.quantity,
|
||||||
|
symbol: item.symbol,
|
||||||
|
tags: item.tags,
|
||||||
|
transactionCount: item.transactionCount,
|
||||||
|
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
|
||||||
|
item.quantity
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
(hasErrors ||
|
||||||
|
currentRateErrors.find(({ dataSource, symbol }) => {
|
||||||
|
return dataSource === item.dataSource && symbol === item.symbol;
|
||||||
|
})) &&
|
||||||
|
item.investment.gt(0)
|
||||||
|
) {
|
||||||
|
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overall = this.calculateOverallPerformance(positions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...overall,
|
||||||
|
errors,
|
||||||
|
positions,
|
||||||
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDataProviderInfos() {
|
||||||
|
return this.dataProviderInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInvestments(): { date: string; investment: Big }[] {
|
||||||
|
if (this.transactionPoints.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.transactionPoints.map((transactionPoint) => {
|
||||||
|
return {
|
||||||
|
date: transactionPoint.date,
|
||||||
|
investment: transactionPoint.items.reduce(
|
||||||
|
(investment, transactionPointSymbol) =>
|
||||||
|
investment.plus(transactionPointSymbol.investment),
|
||||||
|
new Big(0)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInvestmentsByGroup({
|
||||||
|
data,
|
||||||
|
groupBy
|
||||||
|
}: {
|
||||||
|
data: HistoricalDataItem[];
|
||||||
|
groupBy: GroupBy;
|
||||||
|
}): InvestmentItem[] {
|
||||||
|
const groupedData: { [dateGroup: string]: Big } = {};
|
||||||
|
|
||||||
|
for (const { date, investmentValueWithCurrencyEffect } of data) {
|
||||||
|
const dateGroup =
|
||||||
|
groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4);
|
||||||
|
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus(
|
||||||
|
investmentValueWithCurrencyEffect
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(groupedData).map((dateGroup) => ({
|
||||||
|
date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`,
|
||||||
|
investment: groupedData[dateGroup].toNumber()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStartDate() {
|
||||||
|
return this.transactionPoints.length > 0
|
||||||
|
? parseDate(this.transactionPoints[0].date)
|
||||||
|
: new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract getSymbolMetrics({
|
||||||
|
dataSource,
|
||||||
|
end,
|
||||||
|
exchangeRates,
|
||||||
|
isChartMode,
|
||||||
|
marketSymbolMap,
|
||||||
|
start,
|
||||||
|
step,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
end: Date;
|
||||||
|
exchangeRates: { [dateString: string]: number };
|
||||||
|
isChartMode?: boolean;
|
||||||
|
marketSymbolMap: {
|
||||||
|
[date: string]: { [symbol: string]: Big };
|
||||||
|
};
|
||||||
|
start: Date;
|
||||||
|
step?: number;
|
||||||
|
} & UniqueAsset): SymbolMetrics;
|
||||||
|
|
||||||
|
public getTransactionPoints() {
|
||||||
|
return this.transactionPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeTransactionPoints() {
|
||||||
|
this.transactionPoints = [];
|
||||||
|
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
|
||||||
|
|
||||||
|
let lastDate: string = null;
|
||||||
|
let lastTransactionPoint: TransactionPoint = null;
|
||||||
|
|
||||||
|
for (const {
|
||||||
|
fee,
|
||||||
|
date,
|
||||||
|
quantity,
|
||||||
|
SymbolProfile,
|
||||||
|
tags,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
} of this.orders) {
|
||||||
|
let currentTransactionPointItem: TransactionPointSymbol;
|
||||||
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||||
|
|
||||||
|
const factor = getFactor(type);
|
||||||
|
|
||||||
|
if (oldAccumulatedSymbol) {
|
||||||
|
let investment = oldAccumulatedSymbol.investment;
|
||||||
|
|
||||||
|
const newQuantity = quantity
|
||||||
|
.mul(factor)
|
||||||
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
|
|
||||||
|
if (type === 'BUY') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.plus(
|
||||||
|
quantity.mul(unitPrice)
|
||||||
|
);
|
||||||
|
} else if (type === 'SELL') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.minus(
|
||||||
|
quantity.mul(oldAccumulatedSymbol.averagePrice)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTransactionPointItem = {
|
||||||
|
investment,
|
||||||
|
tags,
|
||||||
|
averagePrice: newQuantity.gt(0)
|
||||||
|
? investment.div(newQuantity)
|
||||||
|
: new Big(0),
|
||||||
|
currency: SymbolProfile.currency,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
dividend: new Big(0),
|
||||||
|
fee: fee.plus(oldAccumulatedSymbol.fee),
|
||||||
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
|
quantity: newQuantity,
|
||||||
|
symbol: SymbolProfile.symbol,
|
||||||
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentTransactionPointItem = {
|
||||||
|
fee,
|
||||||
|
tags,
|
||||||
|
averagePrice: unitPrice,
|
||||||
|
currency: SymbolProfile.currency,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
dividend: new Big(0),
|
||||||
|
firstBuyDate: date,
|
||||||
|
investment: unitPrice.mul(quantity).mul(factor),
|
||||||
|
quantity: quantity.mul(factor),
|
||||||
|
symbol: SymbolProfile.symbol,
|
||||||
|
transactionCount: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
symbols[SymbolProfile.symbol] = currentTransactionPointItem;
|
||||||
|
|
||||||
|
const items = lastTransactionPoint?.items ?? [];
|
||||||
|
|
||||||
|
const newItems = items.filter(({ symbol }) => {
|
||||||
|
return symbol !== SymbolProfile.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
|
newItems.push(currentTransactionPointItem);
|
||||||
|
|
||||||
|
newItems.sort((a, b) => {
|
||||||
|
return a.symbol?.localeCompare(b.symbol);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastDate !== date || lastTransactionPoint === null) {
|
||||||
|
lastTransactionPoint = {
|
||||||
|
date,
|
||||||
|
items: newItems
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transactionPoints.push(lastTransactionPoint);
|
||||||
|
} else {
|
||||||
|
lastTransactionPoint.items = newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDate = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -30,54 +37,66 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy and sell in two activities', async () => {
|
it.only('with BALN.SW buy and sell in two activities', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2021-11-22'),
|
||||||
{
|
fee: 1.55,
|
||||||
date: new Date('2021-11-22'),
|
quantity: 2,
|
||||||
fee: 1.55,
|
SymbolProfile: {
|
||||||
quantity: 2,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'CHF',
|
||||||
currency: 'CHF',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Bâloise Holding AG',
|
||||||
name: 'Bâloise Holding AG',
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW'
|
|
||||||
},
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: 142.9
|
|
||||||
},
|
},
|
||||||
{
|
type: 'BUY',
|
||||||
date: new Date('2021-11-30'),
|
unitPrice: 142.9
|
||||||
fee: 1.65,
|
},
|
||||||
quantity: 1,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'CHF',
|
date: new Date('2021-11-30'),
|
||||||
dataSource: 'YAHOO',
|
fee: 1.65,
|
||||||
name: 'Bâloise Holding AG',
|
quantity: 1,
|
||||||
symbol: 'BALN.SW'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'SELL',
|
currency: 'CHF',
|
||||||
unitPrice: 136.6
|
dataSource: 'YAHOO',
|
||||||
|
name: 'Bâloise Holding AG',
|
||||||
|
symbol: 'BALN.SW'
|
||||||
},
|
},
|
||||||
{
|
type: 'SELL',
|
||||||
date: new Date('2021-11-30'),
|
unitPrice: 136.6
|
||||||
fee: 0,
|
},
|
||||||
quantity: 1,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'CHF',
|
date: new Date('2021-11-30'),
|
||||||
dataSource: 'YAHOO',
|
fee: 0,
|
||||||
name: 'Bâloise Holding AG',
|
quantity: 1,
|
||||||
symbol: 'BALN.SW'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'SELL',
|
currency: 'CHF',
|
||||||
unitPrice: 136.6
|
dataSource: 'YAHOO',
|
||||||
}
|
name: 'Bâloise Holding AG',
|
||||||
],
|
symbol: 'BALN.SW'
|
||||||
|
},
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: 136.6
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -30,41 +37,51 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy and sell', async () => {
|
it.only('with BALN.SW buy and sell', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2021-11-22'),
|
||||||
{
|
fee: 1.55,
|
||||||
date: new Date('2021-11-22'),
|
quantity: 2,
|
||||||
fee: 1.55,
|
SymbolProfile: {
|
||||||
quantity: 2,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'CHF',
|
||||||
currency: 'CHF',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Bâloise Holding AG',
|
||||||
name: 'Bâloise Holding AG',
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW'
|
|
||||||
},
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: 142.9
|
|
||||||
},
|
},
|
||||||
{
|
type: 'BUY',
|
||||||
date: new Date('2021-11-30'),
|
unitPrice: 142.9
|
||||||
fee: 1.65,
|
},
|
||||||
quantity: 2,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'CHF',
|
date: new Date('2021-11-30'),
|
||||||
dataSource: 'YAHOO',
|
fee: 1.65,
|
||||||
name: 'Bâloise Holding AG',
|
quantity: 2,
|
||||||
symbol: 'BALN.SW'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'SELL',
|
currency: 'CHF',
|
||||||
unitPrice: 136.6
|
dataSource: 'YAHOO',
|
||||||
}
|
name: 'Bâloise Holding AG',
|
||||||
],
|
symbol: 'BALN.SW'
|
||||||
|
},
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: 136.6
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -30,28 +37,36 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy', async () => {
|
it.only('with BALN.SW buy', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2021-11-30'),
|
||||||
{
|
fee: 1.55,
|
||||||
date: new Date('2021-11-30'),
|
quantity: 2,
|
||||||
fee: 1.55,
|
SymbolProfile: {
|
||||||
quantity: 2,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'CHF',
|
||||||
currency: 'CHF',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Bâloise Holding AG',
|
||||||
name: 'Bâloise Holding AG',
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW'
|
},
|
||||||
},
|
type: 'BUY',
|
||||||
type: 'BUY',
|
unitPrice: 136.6
|
||||||
unitPrice: 136.6
|
}
|
||||||
}
|
];
|
||||||
],
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -7,8 +15,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -33,6 +39,7 @@ jest.mock(
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -43,41 +50,51 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BTCUSD buy and sell partially', async () => {
|
it.only('with BTCUSD buy and sell partially', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2015-01-01'),
|
||||||
{
|
fee: 0,
|
||||||
date: new Date('2015-01-01'),
|
quantity: 2,
|
||||||
fee: 0,
|
SymbolProfile: {
|
||||||
quantity: 2,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'USD',
|
||||||
currency: 'USD',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Bitcoin USD',
|
||||||
name: 'Bitcoin USD',
|
symbol: 'BTCUSD'
|
||||||
symbol: 'BTCUSD'
|
|
||||||
},
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: 320.43
|
|
||||||
},
|
},
|
||||||
{
|
type: 'BUY',
|
||||||
date: new Date('2017-12-31'),
|
unitPrice: 320.43
|
||||||
fee: 0,
|
},
|
||||||
quantity: 1,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'USD',
|
date: new Date('2017-12-31'),
|
||||||
dataSource: 'YAHOO',
|
fee: 0,
|
||||||
name: 'Bitcoin USD',
|
quantity: 1,
|
||||||
symbol: 'BTCUSD'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'SELL',
|
currency: 'USD',
|
||||||
unitPrice: 14156.4
|
dataSource: 'YAHOO',
|
||||||
}
|
name: 'Bitcoin USD',
|
||||||
],
|
symbol: 'BTCUSD'
|
||||||
|
},
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: 14156.4
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -7,8 +15,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -33,6 +39,7 @@ jest.mock(
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -43,28 +50,36 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with GOOGL buy', async () => {
|
it.only('with GOOGL buy', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2023-01-03'),
|
||||||
{
|
fee: 1,
|
||||||
date: new Date('2023-01-03'),
|
quantity: 1,
|
||||||
fee: 1,
|
SymbolProfile: {
|
||||||
quantity: 1,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'USD',
|
||||||
currency: 'USD',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Alphabet Inc.',
|
||||||
name: 'Alphabet Inc.',
|
symbol: 'GOOGL'
|
||||||
symbol: 'GOOGL'
|
},
|
||||||
},
|
type: 'BUY',
|
||||||
type: 'BUY',
|
unitPrice: 89.12
|
||||||
unitPrice: 89.12
|
}
|
||||||
}
|
];
|
||||||
],
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -7,8 +15,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -33,6 +39,7 @@ jest.mock(
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -43,41 +50,51 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with MSFT buy', async () => {
|
it.only('with MSFT buy', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2021-09-16'),
|
||||||
{
|
fee: 19,
|
||||||
date: new Date('2021-09-16'),
|
quantity: 1,
|
||||||
fee: 19,
|
SymbolProfile: {
|
||||||
quantity: 1,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'USD',
|
||||||
currency: 'USD',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Microsoft Inc.',
|
||||||
name: 'Microsoft Inc.',
|
symbol: 'MSFT'
|
||||||
symbol: 'MSFT'
|
|
||||||
},
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: 298.58
|
|
||||||
},
|
},
|
||||||
{
|
type: 'BUY',
|
||||||
date: new Date('2021-11-16'),
|
unitPrice: 298.58
|
||||||
fee: 0,
|
},
|
||||||
quantity: 1,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'USD',
|
date: new Date('2021-11-16'),
|
||||||
dataSource: 'YAHOO',
|
fee: 0,
|
||||||
name: 'Microsoft Inc.',
|
quantity: 1,
|
||||||
symbol: 'MSFT'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'DIVIDEND',
|
currency: 'USD',
|
||||||
unitPrice: 0.62
|
dataSource: 'YAHOO',
|
||||||
}
|
name: 'Microsoft Inc.',
|
||||||
],
|
symbol: 'MSFT'
|
||||||
|
},
|
||||||
|
type: 'DIVIDEND',
|
||||||
|
unitPrice: 0.62
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD'
|
currency: 'USD'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,8 +10,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -20,6 +22,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -30,14 +33,18 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it('with no orders', async () => {
|
it('with no orders', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
currentRateService,
|
|
||||||
exchangeRateDataService,
|
|
||||||
activities: [],
|
activities: [],
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +80,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
netPerformanceWithCurrencyEffect: new Big(0),
|
netPerformanceWithCurrencyEffect: new Big(0),
|
||||||
positions: [],
|
positions: [],
|
||||||
totalInvestment: new Big(0)
|
totalInvestment: new Big(0),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([]);
|
expect(investments).toEqual([]);
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -30,44 +37,53 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with NOVN.SW buy and sell partially', async () => {
|
it.only('with NOVN.SW buy and sell partially', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2022-03-07'),
|
||||||
{
|
fee: 1.3,
|
||||||
date: new Date('2022-03-07'),
|
quantity: 2,
|
||||||
fee: 1.3,
|
SymbolProfile: {
|
||||||
quantity: 2,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'CHF',
|
||||||
currency: 'CHF',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Novartis AG',
|
||||||
name: 'Novartis AG',
|
symbol: 'NOVN.SW'
|
||||||
symbol: 'NOVN.SW'
|
|
||||||
},
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: 75.8
|
|
||||||
},
|
},
|
||||||
{
|
type: 'BUY',
|
||||||
date: new Date('2022-04-08'),
|
unitPrice: 75.8
|
||||||
fee: 2.95,
|
},
|
||||||
quantity: 1,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'CHF',
|
date: new Date('2022-04-08'),
|
||||||
dataSource: 'YAHOO',
|
fee: 2.95,
|
||||||
name: 'Novartis AG',
|
quantity: 1,
|
||||||
symbol: 'NOVN.SW'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'SELL',
|
currency: 'CHF',
|
||||||
unitPrice: 85.73
|
dataSource: 'YAHOO',
|
||||||
}
|
name: 'Novartis AG',
|
||||||
],
|
symbol: 'NOVN.SW'
|
||||||
|
},
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: 85.73
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -30,41 +37,51 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with NOVN.SW buy and sell', async () => {
|
it.only('with NOVN.SW buy and sell', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const activities: Activity[] = [
|
||||||
currentRateService,
|
{
|
||||||
exchangeRateDataService,
|
...activityDummyData,
|
||||||
activities: <Activity[]>[
|
date: new Date('2022-03-07'),
|
||||||
{
|
fee: 0,
|
||||||
date: new Date('2022-03-07'),
|
quantity: 2,
|
||||||
fee: 0,
|
SymbolProfile: {
|
||||||
quantity: 2,
|
...symbolProfileDummyData,
|
||||||
SymbolProfile: {
|
currency: 'CHF',
|
||||||
currency: 'CHF',
|
dataSource: 'YAHOO',
|
||||||
dataSource: 'YAHOO',
|
name: 'Novartis AG',
|
||||||
name: 'Novartis AG',
|
symbol: 'NOVN.SW'
|
||||||
symbol: 'NOVN.SW'
|
|
||||||
},
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: 75.8
|
|
||||||
},
|
},
|
||||||
{
|
type: 'BUY',
|
||||||
date: new Date('2022-04-08'),
|
unitPrice: 75.8
|
||||||
fee: 0,
|
},
|
||||||
quantity: 2,
|
{
|
||||||
SymbolProfile: {
|
...activityDummyData,
|
||||||
currency: 'CHF',
|
date: new Date('2022-04-08'),
|
||||||
dataSource: 'YAHOO',
|
fee: 0,
|
||||||
name: 'Novartis AG',
|
quantity: 2,
|
||||||
symbol: 'NOVN.SW'
|
SymbolProfile: {
|
||||||
},
|
...symbolProfileDummyData,
|
||||||
type: 'SELL',
|
currency: 'CHF',
|
||||||
unitPrice: 85.73
|
dataSource: 'YAHOO',
|
||||||
}
|
name: 'Novartis AG',
|
||||||
],
|
symbol: 'NOVN.SW'
|
||||||
|
},
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: 85.73
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
@ -18,17 +21,21 @@ describe('PortfolioCalculator', () => {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('annualized performance percentage', () => {
|
describe('annualized performance percentage', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
|
||||||
activities: [],
|
|
||||||
currentRateService,
|
|
||||||
exchangeRateDataService,
|
|
||||||
currency: 'USD'
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Get annualized performance', async () => {
|
it('Get annualized performance', async () => {
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities: [],
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
|
currency: 'CHF'
|
||||||
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
portfolioCalculator
|
portfolioCalculator
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
|
@ -1,24 +1,13 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
|
||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator.interface';
|
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
|
||||||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
|
||||||
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
|
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
|
||||||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
|
||||||
import {
|
import {
|
||||||
DataProviderInfo,
|
|
||||||
HistoricalDataItem,
|
|
||||||
InvestmentItem,
|
|
||||||
ResponseError,
|
|
||||||
SymbolMetrics,
|
SymbolMetrics,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { GroupBy } from '@ghostfolio/common/types';
|
|
||||||
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
@ -26,638 +15,15 @@ import {
|
|||||||
addDays,
|
addDays,
|
||||||
addMilliseconds,
|
addMilliseconds,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
eachDayOfInterval,
|
|
||||||
endOfDay,
|
|
||||||
format,
|
format,
|
||||||
isBefore,
|
isBefore
|
||||||
isSameDay,
|
|
||||||
max,
|
|
||||||
subDays
|
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
|
import { cloneDeep, first, last, sortBy } from 'lodash';
|
||||||
|
|
||||||
export class PortfolioCalculator {
|
export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||||
private static readonly ENABLE_LOGGING = false;
|
protected calculateOverallPerformance(
|
||||||
|
positions: TimelinePosition[]
|
||||||
private currency: string;
|
): CurrentPositions {
|
||||||
private currentRateService: CurrentRateService;
|
|
||||||
private dataProviderInfos: DataProviderInfo[];
|
|
||||||
private exchangeRateDataService: ExchangeRateDataService;
|
|
||||||
private orders: PortfolioOrder[];
|
|
||||||
private transactionPoints: TransactionPoint[];
|
|
||||||
|
|
||||||
public constructor({
|
|
||||||
activities,
|
|
||||||
currency,
|
|
||||||
currentRateService,
|
|
||||||
exchangeRateDataService
|
|
||||||
}: {
|
|
||||||
activities: Activity[];
|
|
||||||
currency: string;
|
|
||||||
currentRateService: CurrentRateService;
|
|
||||||
exchangeRateDataService: ExchangeRateDataService;
|
|
||||||
}) {
|
|
||||||
this.currency = currency;
|
|
||||||
this.currentRateService = currentRateService;
|
|
||||||
this.exchangeRateDataService = exchangeRateDataService;
|
|
||||||
this.orders = activities.map(
|
|
||||||
({ date, fee, quantity, SymbolProfile, type, unitPrice }) => {
|
|
||||||
return {
|
|
||||||
SymbolProfile,
|
|
||||||
type,
|
|
||||||
date: format(date, DATE_FORMAT),
|
|
||||||
fee: new Big(fee),
|
|
||||||
quantity: new Big(quantity),
|
|
||||||
unitPrice: new Big(unitPrice)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.orders.sort((a, b) => {
|
|
||||||
return a.date?.localeCompare(b.date);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.computeTransactionPoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAnnualizedPerformancePercent({
|
|
||||||
daysInMarket,
|
|
||||||
netPerformancePercent
|
|
||||||
}: {
|
|
||||||
daysInMarket: number;
|
|
||||||
netPerformancePercent: Big;
|
|
||||||
}): Big {
|
|
||||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
|
||||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
|
||||||
return new Big(
|
|
||||||
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
|
||||||
).minus(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Big(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const currentDate of dates) {
|
|
||||||
const dateString = format(currentDate, DATE_FORMAT);
|
|
||||||
|
|
||||||
for (const symbol of Object.keys(valuesBySymbol)) {
|
|
||||||
const symbolValues = valuesBySymbol[symbol];
|
|
||||||
|
|
||||||
const currentValue =
|
|
||||||
symbolValues.currentValues?.[dateString] ?? new Big(0);
|
|
||||||
|
|
||||||
const currentValueWithCurrencyEffect =
|
|
||||||
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ??
|
|
||||||
new Big(0);
|
|
||||||
|
|
||||||
const investmentValueAccumulated =
|
|
||||||
symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0);
|
|
||||||
|
|
||||||
const investmentValueAccumulatedWithCurrencyEffect =
|
|
||||||
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[
|
|
||||||
dateString
|
|
||||||
] ?? new Big(0);
|
|
||||||
|
|
||||||
const investmentValueWithCurrencyEffect =
|
|
||||||
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ??
|
|
||||||
new Big(0);
|
|
||||||
|
|
||||||
const netPerformanceValue =
|
|
||||||
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
|
|
||||||
|
|
||||||
const netPerformanceValueWithCurrencyEffect =
|
|
||||||
symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ??
|
|
||||||
new Big(0);
|
|
||||||
|
|
||||||
const timeWeightedInvestmentValue =
|
|
||||||
symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0);
|
|
||||||
|
|
||||||
const timeWeightedInvestmentValueWithCurrencyEffect =
|
|
||||||
symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[
|
|
||||||
dateString
|
|
||||||
] ?? new Big(0);
|
|
||||||
|
|
||||||
accumulatedValuesByDate[dateString] = {
|
|
||||||
investmentValueWithCurrencyEffect: (
|
|
||||||
accumulatedValuesByDate[dateString]
|
|
||||||
?.investmentValueWithCurrencyEffect ?? new Big(0)
|
|
||||||
).add(investmentValueWithCurrencyEffect),
|
|
||||||
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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
|
|
||||||
const {
|
|
||||||
investmentValueWithCurrencyEffect,
|
|
||||||
totalCurrentValue,
|
|
||||||
totalCurrentValueWithCurrencyEffect,
|
|
||||||
totalInvestmentValue,
|
|
||||||
totalInvestmentValueWithCurrencyEffect,
|
|
||||||
totalNetPerformanceValue,
|
|
||||||
totalNetPerformanceValueWithCurrencyEffect,
|
|
||||||
totalTimeWeightedInvestmentValue,
|
|
||||||
totalTimeWeightedInvestmentValueWithCurrencyEffect
|
|
||||||
} = values;
|
|
||||||
|
|
||||||
const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0)
|
|
||||||
? 0
|
|
||||||
: totalNetPerformanceValue
|
|
||||||
.div(totalTimeWeightedInvestmentValue)
|
|
||||||
.mul(100)
|
|
||||||
.toNumber();
|
|
||||||
|
|
||||||
const netPerformanceInPercentageWithCurrencyEffect =
|
|
||||||
totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0)
|
|
||||||
? 0
|
|
||||||
: totalNetPerformanceValueWithCurrencyEffect
|
|
||||||
.div(totalTimeWeightedInvestmentValueWithCurrencyEffect)
|
|
||||||
.mul(100)
|
|
||||||
.toNumber();
|
|
||||||
|
|
||||||
return {
|
|
||||||
date,
|
|
||||||
netPerformanceInPercentage,
|
|
||||||
netPerformanceInPercentageWithCurrencyEffect,
|
|
||||||
investmentValueWithCurrencyEffect:
|
|
||||||
investmentValueWithCurrencyEffect.toNumber(),
|
|
||||||
netPerformance: totalNetPerformanceValue.toNumber(),
|
|
||||||
netPerformanceWithCurrencyEffect:
|
|
||||||
totalNetPerformanceValueWithCurrencyEffect.toNumber(),
|
|
||||||
totalInvestment: totalInvestmentValue.toNumber(),
|
|
||||||
totalInvestmentValueWithCurrencyEffect:
|
|
||||||
totalInvestmentValueWithCurrencyEffect.toNumber(),
|
|
||||||
value: totalCurrentValue.toNumber(),
|
|
||||||
valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getCurrentPositions(
|
|
||||||
start: Date,
|
|
||||||
end?: Date
|
|
||||||
): Promise<CurrentPositions> {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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),
|
|
||||||
positions: [],
|
|
||||||
totalInvestment: new Big(0)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const currencies: { [symbol: string]: string } = {};
|
|
||||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
|
||||||
let dates: Date[] = [];
|
|
||||||
let firstIndex = transactionPoints.length;
|
|
||||||
let firstTransactionPoint: TransactionPoint = null;
|
|
||||||
|
|
||||||
dates.push(resetHours(start));
|
|
||||||
|
|
||||||
for (const { currency, dataSource, symbol } of transactionPoints[
|
|
||||||
firstIndex - 1
|
|
||||||
].items) {
|
|
||||||
dataGatheringItems.push({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
});
|
|
||||||
|
|
||||||
currencies[symbol] = currency;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < transactionPoints.length; i++) {
|
|
||||||
if (
|
|
||||||
!isBefore(parseDate(transactionPoints[i].date), start) &&
|
|
||||||
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(),
|
|
||||||
targetCurrency: this.currency
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
dataProviderInfos,
|
|
||||||
errors: currentRateErrors,
|
|
||||||
values: marketSymbols
|
|
||||||
} = await this.currentRateService.getValues({
|
|
||||||
dataGatheringItems,
|
|
||||||
dateQuery: {
|
|
||||||
in: dates
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dataProviderInfos = dataProviderInfos;
|
|
||||||
|
|
||||||
const marketSymbolMap: {
|
|
||||||
[date: string]: { [symbol: string]: Big };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
for (const marketSymbol of marketSymbols) {
|
|
||||||
const date = format(marketSymbol.date, DATE_FORMAT);
|
|
||||||
|
|
||||||
if (!marketSymbolMap[date]) {
|
|
||||||
marketSymbolMap[date] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (marketSymbol.marketPrice) {
|
|
||||||
marketSymbolMap[date][marketSymbol.symbol] = new Big(
|
|
||||||
marketSymbol.marketPrice
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const endDateString = format(endDate, DATE_FORMAT);
|
|
||||||
|
|
||||||
if (firstIndex > 0) {
|
|
||||||
firstIndex--;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions: TimelinePosition[] = [];
|
|
||||||
let hasAnySymbolMetricsErrors = false;
|
|
||||||
|
|
||||||
const errors: ResponseError['errors'] = [];
|
|
||||||
|
|
||||||
for (const item of lastTransactionPoint.items) {
|
|
||||||
const marketPriceInBaseCurrency = (
|
|
||||||
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
|
|
||||||
).mul(
|
|
||||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
|
||||||
endDateString
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
grossPerformance,
|
|
||||||
grossPerformancePercentage,
|
|
||||||
grossPerformancePercentageWithCurrencyEffect,
|
|
||||||
grossPerformanceWithCurrencyEffect,
|
|
||||||
hasErrors,
|
|
||||||
netPerformance,
|
|
||||||
netPerformancePercentage,
|
|
||||||
netPerformancePercentageWithCurrencyEffect,
|
|
||||||
netPerformanceWithCurrencyEffect,
|
|
||||||
timeWeightedInvestment,
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect,
|
|
||||||
totalDividend,
|
|
||||||
totalDividendInBaseCurrency,
|
|
||||||
totalInvestment,
|
|
||||||
totalInvestmentWithCurrencyEffect
|
|
||||||
} = this.getSymbolMetrics({
|
|
||||||
marketSymbolMap,
|
|
||||||
start,
|
|
||||||
dataSource: item.dataSource,
|
|
||||||
end: endDate,
|
|
||||||
exchangeRates:
|
|
||||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
|
||||||
symbol: item.symbol
|
|
||||||
});
|
|
||||||
|
|
||||||
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
|
||||||
|
|
||||||
positions.push({
|
|
||||||
dividend: totalDividend,
|
|
||||||
dividendInBaseCurrency: totalDividendInBaseCurrency,
|
|
||||||
timeWeightedInvestment,
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect,
|
|
||||||
averagePrice: item.averagePrice,
|
|
||||||
currency: item.currency,
|
|
||||||
dataSource: item.dataSource,
|
|
||||||
fee: item.fee,
|
|
||||||
firstBuyDate: item.firstBuyDate,
|
|
||||||
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
|
|
||||||
grossPerformancePercentage: !hasErrors
|
|
||||||
? grossPerformancePercentage ?? null
|
|
||||||
: null,
|
|
||||||
grossPerformancePercentageWithCurrencyEffect: !hasErrors
|
|
||||||
? grossPerformancePercentageWithCurrencyEffect ?? null
|
|
||||||
: null,
|
|
||||||
grossPerformanceWithCurrencyEffect: !hasErrors
|
|
||||||
? grossPerformanceWithCurrencyEffect ?? null
|
|
||||||
: null,
|
|
||||||
investment: totalInvestment,
|
|
||||||
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
|
|
||||||
marketPrice:
|
|
||||||
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
|
|
||||||
marketPriceInBaseCurrency:
|
|
||||||
marketPriceInBaseCurrency?.toNumber() ?? null,
|
|
||||||
netPerformance: !hasErrors ? netPerformance ?? null : null,
|
|
||||||
netPerformancePercentage: !hasErrors
|
|
||||||
? netPerformancePercentage ?? null
|
|
||||||
: null,
|
|
||||||
netPerformancePercentageWithCurrencyEffect: !hasErrors
|
|
||||||
? netPerformancePercentageWithCurrencyEffect ?? null
|
|
||||||
: null,
|
|
||||||
netPerformanceWithCurrencyEffect: !hasErrors
|
|
||||||
? netPerformanceWithCurrencyEffect ?? null
|
|
||||||
: null,
|
|
||||||
quantity: item.quantity,
|
|
||||||
symbol: item.symbol,
|
|
||||||
tags: item.tags,
|
|
||||||
transactionCount: item.transactionCount,
|
|
||||||
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
|
|
||||||
item.quantity
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
(hasErrors ||
|
|
||||||
currentRateErrors.find(({ dataSource, symbol }) => {
|
|
||||||
return dataSource === item.dataSource && symbol === item.symbol;
|
|
||||||
})) &&
|
|
||||||
item.investment.gt(0)
|
|
||||||
) {
|
|
||||||
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const overall = this.calculateOverallPerformance(positions);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...overall,
|
|
||||||
errors,
|
|
||||||
positions,
|
|
||||||
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDataProviderInfos() {
|
|
||||||
return this.dataProviderInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInvestments(): { date: string; investment: Big }[] {
|
|
||||||
if (this.transactionPoints.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.transactionPoints.map((transactionPoint) => {
|
|
||||||
return {
|
|
||||||
date: transactionPoint.date,
|
|
||||||
investment: transactionPoint.items.reduce(
|
|
||||||
(investment, transactionPointSymbol) =>
|
|
||||||
investment.plus(transactionPointSymbol.investment),
|
|
||||||
new Big(0)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInvestmentsByGroup({
|
|
||||||
data,
|
|
||||||
groupBy
|
|
||||||
}: {
|
|
||||||
data: HistoricalDataItem[];
|
|
||||||
groupBy: GroupBy;
|
|
||||||
}): InvestmentItem[] {
|
|
||||||
const groupedData: { [dateGroup: string]: Big } = {};
|
|
||||||
|
|
||||||
for (const { date, investmentValueWithCurrencyEffect } of data) {
|
|
||||||
const dateGroup =
|
|
||||||
groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4);
|
|
||||||
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus(
|
|
||||||
investmentValueWithCurrencyEffect
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(groupedData).map((dateGroup) => ({
|
|
||||||
date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`,
|
|
||||||
investment: groupedData[dateGroup].toNumber()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateOverallPerformance(positions: TimelinePosition[]) {
|
|
||||||
let currentValueInBaseCurrency = new Big(0);
|
let currentValueInBaseCurrency = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformanceWithCurrencyEffect = new Big(0);
|
let grossPerformanceWithCurrencyEffect = new Big(0);
|
||||||
@ -754,119 +120,12 @@ export class PortfolioCalculator {
|
|||||||
? new Big(0)
|
? new Big(0)
|
||||||
: grossPerformanceWithCurrencyEffect.div(
|
: grossPerformanceWithCurrencyEffect.div(
|
||||||
totalTimeWeightedInvestmentWithCurrencyEffect
|
totalTimeWeightedInvestmentWithCurrencyEffect
|
||||||
)
|
),
|
||||||
|
positions
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStartDate() {
|
protected getSymbolMetrics({
|
||||||
return this.transactionPoints.length > 0
|
|
||||||
? parseDate(this.transactionPoints[0].date)
|
|
||||||
: new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTransactionPoints() {
|
|
||||||
return this.transactionPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeTransactionPoints() {
|
|
||||||
this.transactionPoints = [];
|
|
||||||
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
|
|
||||||
|
|
||||||
let lastDate: string = null;
|
|
||||||
let lastTransactionPoint: TransactionPoint = null;
|
|
||||||
|
|
||||||
for (const {
|
|
||||||
fee,
|
|
||||||
date,
|
|
||||||
quantity,
|
|
||||||
SymbolProfile,
|
|
||||||
tags,
|
|
||||||
type,
|
|
||||||
unitPrice
|
|
||||||
} of this.orders) {
|
|
||||||
let currentTransactionPointItem: TransactionPointSymbol;
|
|
||||||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
|
||||||
|
|
||||||
const factor = getFactor(type);
|
|
||||||
|
|
||||||
if (oldAccumulatedSymbol) {
|
|
||||||
let investment = oldAccumulatedSymbol.investment;
|
|
||||||
|
|
||||||
const newQuantity = quantity
|
|
||||||
.mul(factor)
|
|
||||||
.plus(oldAccumulatedSymbol.quantity);
|
|
||||||
|
|
||||||
if (type === 'BUY') {
|
|
||||||
investment = oldAccumulatedSymbol.investment.plus(
|
|
||||||
quantity.mul(unitPrice)
|
|
||||||
);
|
|
||||||
} else if (type === 'SELL') {
|
|
||||||
investment = oldAccumulatedSymbol.investment.minus(
|
|
||||||
quantity.mul(oldAccumulatedSymbol.averagePrice)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTransactionPointItem = {
|
|
||||||
investment,
|
|
||||||
tags,
|
|
||||||
averagePrice: newQuantity.gt(0)
|
|
||||||
? investment.div(newQuantity)
|
|
||||||
: new Big(0),
|
|
||||||
currency: SymbolProfile.currency,
|
|
||||||
dataSource: SymbolProfile.dataSource,
|
|
||||||
dividend: new Big(0),
|
|
||||||
fee: fee.plus(oldAccumulatedSymbol.fee),
|
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
|
||||||
quantity: newQuantity,
|
|
||||||
symbol: SymbolProfile.symbol,
|
|
||||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
currentTransactionPointItem = {
|
|
||||||
fee,
|
|
||||||
tags,
|
|
||||||
averagePrice: unitPrice,
|
|
||||||
currency: SymbolProfile.currency,
|
|
||||||
dataSource: SymbolProfile.dataSource,
|
|
||||||
dividend: new Big(0),
|
|
||||||
firstBuyDate: date,
|
|
||||||
investment: unitPrice.mul(quantity).mul(factor),
|
|
||||||
quantity: quantity.mul(factor),
|
|
||||||
symbol: SymbolProfile.symbol,
|
|
||||||
transactionCount: 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
symbols[SymbolProfile.symbol] = currentTransactionPointItem;
|
|
||||||
|
|
||||||
const items = lastTransactionPoint?.items ?? [];
|
|
||||||
|
|
||||||
const newItems = items.filter(({ symbol }) => {
|
|
||||||
return symbol !== SymbolProfile.symbol;
|
|
||||||
});
|
|
||||||
|
|
||||||
newItems.push(currentTransactionPointItem);
|
|
||||||
|
|
||||||
newItems.sort((a, b) => {
|
|
||||||
return a.symbol?.localeCompare(b.symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lastDate !== date || lastTransactionPoint === null) {
|
|
||||||
lastTransactionPoint = {
|
|
||||||
date,
|
|
||||||
items: newItems
|
|
||||||
};
|
|
||||||
|
|
||||||
this.transactionPoints.push(lastTransactionPoint);
|
|
||||||
} else {
|
|
||||||
lastTransactionPoint.items = newItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastDate = date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSymbolMetrics({
|
|
||||||
dataSource,
|
dataSource,
|
||||||
end,
|
end,
|
||||||
exchangeRates,
|
exchangeRates,
|
||||||
|
@ -16,4 +16,5 @@ export interface CurrentPositions extends ResponseError {
|
|||||||
netPerformancePercentageWithCurrencyEffect: Big;
|
netPerformancePercentageWithCurrencyEffect: Big;
|
||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
totalInvestment: Big;
|
totalInvestment: Big;
|
||||||
|
totalInvestmentWithCurrencyEffect: Big;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/sym
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { PortfolioController } from './portfolio.controller';
|
import { PortfolioController } from './portfolio.controller';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
@ -41,6 +42,7 @@ import { RulesService } from './rules.service';
|
|||||||
AccountBalanceService,
|
AccountBalanceService,
|
||||||
AccountService,
|
AccountService,
|
||||||
CurrentRateService,
|
CurrentRateService,
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
PortfolioService,
|
PortfolioService,
|
||||||
RulesService
|
RulesService
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,6 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import {
|
import {
|
||||||
getFactor,
|
getFactor,
|
||||||
@ -81,7 +80,11 @@ import {
|
|||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty, last, uniq, uniqBy } from 'lodash';
|
import { isEmpty, last, uniq, uniqBy } from 'lodash';
|
||||||
|
|
||||||
import { PortfolioCalculator } from './calculator/twr/portfolio-calculator';
|
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||||
|
import {
|
||||||
|
PerformanceCalculationType,
|
||||||
|
PortfolioCalculatorFactory
|
||||||
|
} from './calculator/portfolio-calculator.factory';
|
||||||
import {
|
import {
|
||||||
HistoricalDataContainer,
|
HistoricalDataContainer,
|
||||||
PortfolioPositionDetail
|
PortfolioPositionDetail
|
||||||
@ -98,7 +101,7 @@ export class PortfolioService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountBalanceService: AccountBalanceService,
|
private readonly accountBalanceService: AccountBalanceService,
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly currentRateService: CurrentRateService,
|
private readonly calculatorFactory: PortfolioCalculatorFactory,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly impersonationService: ImpersonationService,
|
||||||
@ -265,11 +268,10 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currentRateService: this.currentRateService,
|
currency: this.request.user.Settings.settings.baseCurrency
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { items } = await this.getChart({
|
const { items } = await this.getChart({
|
||||||
@ -354,11 +356,10 @@ export class PortfolioService {
|
|||||||
withExcludedAccounts
|
withExcludedAccounts
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
currency: userCurrency,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currentRateService: this.currentRateService,
|
currency: userCurrency
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { startDate } = getInterval(
|
const { startDate } = getInterval(
|
||||||
@ -720,15 +721,14 @@ export class PortfolioService {
|
|||||||
|
|
||||||
tags = uniqBy(tags, 'id');
|
tags = uniqBy(tags, 'id');
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities: orders.filter((order) => {
|
activities: orders.filter((order) => {
|
||||||
tags = tags.concat(order.tags);
|
tags = tags.concat(order.tags);
|
||||||
|
|
||||||
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
||||||
}),
|
}),
|
||||||
currency: userCurrency,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currentRateService: this.currentRateService,
|
currency: userCurrency
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioStart = portfolioCalculator.getStartDate();
|
const portfolioStart = portfolioCalculator.getStartDate();
|
||||||
@ -963,11 +963,10 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currentRateService: this.currentRateService,
|
currency: this.request.user.Settings.settings.baseCurrency
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
@ -1152,11 +1151,10 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
currency: userCurrency,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currentRateService: this.currentRateService,
|
currency: userCurrency
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -1270,11 +1268,10 @@ export class PortfolioService {
|
|||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
currency: userCurrency,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currentRateService: this.currentRateService,
|
currency: this.request.user.Settings.settings.baseCurrency
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
@ -1772,12 +1769,12 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||||
|
|
||||||
const annualizedPerformancePercent = new PortfolioCalculator({
|
const annualizedPerformancePercent = this.calculatorFactory
|
||||||
activities: [],
|
.createCalculator({
|
||||||
currency: userCurrency,
|
activities: [],
|
||||||
currentRateService: this.currentRateService,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
currency: userCurrency
|
||||||
})
|
})
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent: new Big(
|
netPerformancePercent: new Big(
|
||||||
@ -1787,12 +1784,12 @@ export class PortfolioService {
|
|||||||
?.toNumber();
|
?.toNumber();
|
||||||
|
|
||||||
const annualizedPerformancePercentWithCurrencyEffect =
|
const annualizedPerformancePercentWithCurrencyEffect =
|
||||||
new PortfolioCalculator({
|
this.calculatorFactory
|
||||||
activities: [],
|
.createCalculator({
|
||||||
currency: userCurrency,
|
activities: [],
|
||||||
currentRateService: this.currentRateService,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
currency: userCurrency
|
||||||
})
|
})
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent: new Big(
|
netPerformancePercent: new Big(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user