Feature/move dividend fee and interest calculation to portfolio calculator (#3267)
* Move dividend, feee and interest calculation to portfolio calculator * Update changelog
This commit is contained in:
parent
b31bbbe2d1
commit
6c57609db8
@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the dividend calculations into the portfolio calculator
|
||||||
|
- Moved the fee calculations into the portfolio calculator
|
||||||
|
- Moved the interest calculations into the portfolio calculator
|
||||||
|
|
||||||
## 2.72.0 - 2024-04-13
|
## 2.72.0 - 2024-04-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
|
||||||
import {
|
import {
|
||||||
SymbolMetrics,
|
SymbolMetrics,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
export class MWRPortfolioCalculator extends PortfolioCalculator {
|
export class MWRPortfolioCalculator extends PortfolioCalculator {
|
||||||
protected calculateOverallPerformance(
|
protected calculateOverallPerformance(
|
||||||
positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
): CurrentPositions {
|
): PortfolioSnapshot {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
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 { DateRange } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@ -23,17 +24,20 @@ export class PortfolioCalculatorFactory {
|
|||||||
public createCalculator({
|
public createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType,
|
calculationType,
|
||||||
currency
|
currency,
|
||||||
|
dateRange = 'max'
|
||||||
}: {
|
}: {
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
calculationType: PerformanceCalculationType;
|
calculationType: PerformanceCalculationType;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
dateRange?: DateRange;
|
||||||
}): PortfolioCalculator {
|
}): PortfolioCalculator {
|
||||||
switch (calculationType) {
|
switch (calculationType) {
|
||||||
case PerformanceCalculationType.MWR:
|
case PerformanceCalculationType.MWR:
|
||||||
return new MWRPortfolioCalculator({
|
return new MWRPortfolioCalculator({
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
|
dateRange,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
});
|
});
|
||||||
@ -42,6 +46,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
|
dateRange,
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
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 { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
|
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
|
||||||
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.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 { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
import {
|
import {
|
||||||
@ -11,7 +11,12 @@ import {
|
|||||||
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 { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config';
|
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
|
getSum,
|
||||||
|
parseDate,
|
||||||
|
resetHours
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
DataProviderInfo,
|
DataProviderInfo,
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
@ -44,18 +49,24 @@ export abstract class PortfolioCalculator {
|
|||||||
private currency: string;
|
private currency: string;
|
||||||
private currentRateService: CurrentRateService;
|
private currentRateService: CurrentRateService;
|
||||||
private dataProviderInfos: DataProviderInfo[];
|
private dataProviderInfos: DataProviderInfo[];
|
||||||
|
private endDate: Date;
|
||||||
private exchangeRateDataService: ExchangeRateDataService;
|
private exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
private snapshot: PortfolioSnapshot;
|
||||||
|
private snapshotPromise: Promise<void>;
|
||||||
|
private startDate: Date;
|
||||||
private transactionPoints: TransactionPoint[];
|
private transactionPoints: TransactionPoint[];
|
||||||
|
|
||||||
public constructor({
|
public constructor({
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
|
dateRange,
|
||||||
exchangeRateDataService
|
exchangeRateDataService
|
||||||
}: {
|
}: {
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
currency: string;
|
currency: string;
|
||||||
currentRateService: CurrentRateService;
|
currentRateService: CurrentRateService;
|
||||||
|
dateRange: DateRange;
|
||||||
exchangeRateDataService: ExchangeRateDataService;
|
exchangeRateDataService: ExchangeRateDataService;
|
||||||
}) {
|
}) {
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
@ -79,12 +90,270 @@ export abstract class PortfolioCalculator {
|
|||||||
return a.date?.localeCompare(b.date);
|
return a.date?.localeCompare(b.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { endDate, startDate } = getInterval(dateRange);
|
||||||
|
|
||||||
|
this.endDate = endDate;
|
||||||
|
this.startDate = startDate;
|
||||||
|
|
||||||
this.computeTransactionPoints();
|
this.computeTransactionPoints();
|
||||||
|
|
||||||
|
this.snapshotPromise = this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract calculateOverallPerformance(
|
protected abstract calculateOverallPerformance(
|
||||||
positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
): CurrentPositions;
|
): PortfolioSnapshot;
|
||||||
|
|
||||||
|
public async computeSnapshot(
|
||||||
|
start: Date,
|
||||||
|
end?: Date
|
||||||
|
): Promise<PortfolioSnapshot> {
|
||||||
|
const lastTransactionPoint = last(this.transactionPoints);
|
||||||
|
|
||||||
|
let endDate = end;
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
endDate = new Date(Date.now());
|
||||||
|
|
||||||
|
if (lastTransactionPoint) {
|
||||||
|
endDate = max([endDate, parseDate(lastTransactionPoint.date)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
|
||||||
|
return isBefore(parseDate(date), endDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [],
|
||||||
|
totalFeesWithCurrencyEffect: new Big(0),
|
||||||
|
totalInterestWithCurrencyEffect: new Big(0),
|
||||||
|
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,
|
||||||
|
totalInterestWithCurrencyEffect: lastTransactionPoint.interest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async getChart({
|
public async getChart({
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
@ -380,258 +649,32 @@ export abstract class PortfolioCalculator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
public getDataProviderInfos() {
|
||||||
return this.dataProviderInfos;
|
return this.dataProviderInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividendInBaseCurrency() {
|
||||||
|
await this.snapshotPromise;
|
||||||
|
|
||||||
|
return getSum(
|
||||||
|
this.snapshot.positions.map(({ dividendInBaseCurrency }) => {
|
||||||
|
return dividendInBaseCurrency;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFeesInBaseCurrency() {
|
||||||
|
await this.snapshotPromise;
|
||||||
|
|
||||||
|
return this.snapshot.totalFeesWithCurrencyEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getInterestInBaseCurrency() {
|
||||||
|
await this.snapshotPromise;
|
||||||
|
|
||||||
|
return this.snapshot.totalInterestWithCurrencyEffect;
|
||||||
|
}
|
||||||
|
|
||||||
public getInvestments(): { date: string; investment: Big }[] {
|
public getInvestments(): { date: string; investment: Big }[] {
|
||||||
if (this.transactionPoints.length === 0) {
|
if (this.transactionPoints.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@ -672,6 +715,12 @@ export abstract class PortfolioCalculator {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSnapshot() {
|
||||||
|
await this.snapshotPromise;
|
||||||
|
|
||||||
|
return this.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
public getStartDate() {
|
public getStartDate() {
|
||||||
return this.transactionPoints.length > 0
|
return this.transactionPoints.length > 0
|
||||||
? parseDate(this.transactionPoints[0].date)
|
? parseDate(this.transactionPoints[0].date)
|
||||||
@ -718,6 +767,13 @@ export abstract class PortfolioCalculator {
|
|||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of this.orders) {
|
} of this.orders) {
|
||||||
|
if (
|
||||||
|
// TODO
|
||||||
|
['ITEM', 'LIABILITY'].includes(type)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let currentTransactionPointItem: TransactionPointSymbol;
|
let currentTransactionPointItem: TransactionPointSymbol;
|
||||||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||||
|
|
||||||
@ -790,18 +846,39 @@ export abstract class PortfolioCalculator {
|
|||||||
return a.symbol?.localeCompare(b.symbol);
|
return a.symbol?.localeCompare(b.symbol);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let fees = new Big(0);
|
||||||
|
|
||||||
|
if (type === 'FEE') {
|
||||||
|
fees = fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
let interest = new Big(0);
|
||||||
|
|
||||||
|
if (type === 'INTEREST') {
|
||||||
|
interest = quantity.mul(unitPrice);
|
||||||
|
}
|
||||||
|
|
||||||
if (lastDate !== date || lastTransactionPoint === null) {
|
if (lastDate !== date || lastTransactionPoint === null) {
|
||||||
lastTransactionPoint = {
|
lastTransactionPoint = {
|
||||||
date,
|
date,
|
||||||
|
fees,
|
||||||
|
interest,
|
||||||
items: newItems
|
items: newItems
|
||||||
};
|
};
|
||||||
|
|
||||||
this.transactionPoints.push(lastTransactionPoint);
|
this.transactionPoints.push(lastTransactionPoint);
|
||||||
} else {
|
} else {
|
||||||
|
lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees);
|
||||||
|
lastTransactionPoint.interest =
|
||||||
|
lastTransactionPoint.interest.plus(interest);
|
||||||
lastTransactionPoint.items = newItems;
|
lastTransactionPoint.items = newItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDate = date;
|
lastDate = date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initialize() {
|
||||||
|
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
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 spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -100,15 +104,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2021-11-22')
|
start: parseDate('2021-11-22')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2021-11-22')
|
parseDate('2021-11-22')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
@ -173,6 +173,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('0')
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('3.2'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
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 spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2021-11-22')
|
start: parseDate('2021-11-22')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2021-11-22')
|
parseDate('2021-11-22')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
@ -156,6 +156,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('0')
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('3.2'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy', async () => {
|
it.only('with BALN.SW buy', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -70,15 +74,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2021-11-30')
|
start: parseDate('2021-11-30')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2021-11-30')
|
parseDate('2021-11-30')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('297.8'),
|
currentValueInBaseCurrency: new Big('297.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('24.6'),
|
grossPerformance: new Big('24.6'),
|
||||||
@ -141,6 +141,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('297.8')
|
valueInBaseCurrency: new Big('297.8')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('1.55'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('273.2'),
|
totalInvestment: new Big('273.2'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('273.2')
|
totalInvestmentWithCurrencyEffect: new Big('273.2')
|
||||||
});
|
});
|
||||||
|
@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
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 spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -98,15 +102,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2015-01-01')
|
start: parseDate('2015-01-01')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2015-01-01')
|
parseDate('2015-01-01')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('13298.425356'),
|
currentValueInBaseCurrency: new Big('13298.425356'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27172.74'),
|
grossPerformance: new Big('27172.74'),
|
||||||
@ -175,6 +175,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('13298.425356')
|
valueInBaseCurrency: new Big('13298.425356')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('320.43'),
|
totalInvestment: new Big('320.43'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957')
|
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957')
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
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 { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('compute portfolio snapshot', () => {
|
||||||
|
it.only('with fee activity', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
|
const activities: Activity[] = [
|
||||||
|
{
|
||||||
|
...activityDummyData,
|
||||||
|
date: new Date('2021-09-01'),
|
||||||
|
fee: 49,
|
||||||
|
quantity: 0,
|
||||||
|
SymbolProfile: {
|
||||||
|
...symbolProfileDummyData,
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
name: 'Account Opening Fee',
|
||||||
|
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
|
||||||
|
},
|
||||||
|
type: 'FEE',
|
||||||
|
unitPrice: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
|
currency: 'USD'
|
||||||
|
});
|
||||||
|
|
||||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
|
parseDate('2021-11-30')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(portfolioSnapshot).toEqual({
|
||||||
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('0'),
|
||||||
|
grossPerformancePercentage: new Big('0'),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
hasErrors: true,
|
||||||
|
netPerformance: new Big('0'),
|
||||||
|
netPerformancePercentage: new Big('0'),
|
||||||
|
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('0'),
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
|
fee: new Big('49'),
|
||||||
|
firstBuyDate: '2021-09-01',
|
||||||
|
grossPerformance: null,
|
||||||
|
grossPerformancePercentage: null,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
grossPerformanceWithCurrencyEffect: null,
|
||||||
|
investment: new Big('0'),
|
||||||
|
investmentWithCurrencyEffect: new Big('0'),
|
||||||
|
marketPrice: null,
|
||||||
|
marketPriceInBaseCurrency: 0,
|
||||||
|
netPerformance: null,
|
||||||
|
netPerformancePercentage: null,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
netPerformanceWithCurrencyEffect: null,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141',
|
||||||
|
tags: [],
|
||||||
|
timeWeightedInvestment: new Big('0'),
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
transactionCount: 1,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('49'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInvestment: new Big('0'),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with GOOGL buy', async () => {
|
it.only('with GOOGL buy', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -83,15 +87,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2023-01-03')
|
start: parseDate('2023-01-03')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2023-01-03')
|
parseDate('2023-01-03')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('103.10483'),
|
currentValueInBaseCurrency: new Big('103.10483'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27.33'),
|
grossPerformance: new Big('27.33'),
|
||||||
@ -154,6 +154,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('103.10483')
|
valueInBaseCurrency: new Big('103.10483')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('1'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('89.12'),
|
totalInvestment: new Big('89.12'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('82.329056')
|
totalInvestmentWithCurrencyEffect: new Big('82.329056')
|
||||||
});
|
});
|
||||||
|
@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with MSFT buy', async () => {
|
it.only('with MSFT buy', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -98,17 +102,13 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'USD'
|
currency: 'USD'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
||||||
parseDate('2023-07-10')
|
parseDate('2023-07-10')
|
||||||
);
|
);
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toMatchObject({
|
expect(portfolioSnapshot).toMatchObject({
|
||||||
errors: [],
|
errors: [],
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
positions: [
|
positions: [
|
||||||
@ -130,6 +130,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('19'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('298.58'),
|
totalInvestment: new Big('298.58'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('298.58')
|
totalInvestmentWithCurrencyEffect: new Big('298.58')
|
||||||
});
|
});
|
||||||
|
@ -42,22 +42,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it('with no orders', async () => {
|
it('with no orders', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities: [],
|
activities: [],
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const start = subDays(new Date(Date.now()), 10);
|
const start = subDays(new Date(Date.now()), 10);
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({ start });
|
const chartData = await portfolioCalculator.getChartData({ start });
|
||||||
|
|
||||||
const currentPositions =
|
const portfolioSnapshot =
|
||||||
await portfolioCalculator.getCurrentPositions(start);
|
await portfolioCalculator.computeSnapshot(start);
|
||||||
|
|
||||||
const investments = portfolioCalculator.getInvestments();
|
const investments = portfolioCalculator.getInvestments();
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big(0),
|
currentValueInBaseCurrency: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
@ -80,6 +80,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
netPerformanceWithCurrencyEffect: new Big(0),
|
netPerformanceWithCurrencyEffect: new Big(0),
|
||||||
positions: [],
|
positions: [],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big(0),
|
totalInvestment: new Big(0),
|
||||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
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 spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -84,15 +88,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2022-03-07')
|
start: parseDate('2022-03-07')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2022-03-07')
|
parseDate('2022-03-07')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('87.8'),
|
currentValueInBaseCurrency: new Big('87.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('21.93'),
|
grossPerformance: new Big('21.93'),
|
||||||
@ -157,6 +158,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('87.8')
|
valueInBaseCurrency: new Big('87.8')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('4.25'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('75.80'),
|
totalInvestment: new Big('75.80'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('75.80')
|
totalInvestmentWithCurrencyEffect: new Big('75.80')
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
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 spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2022-03-07')
|
start: parseDate('2022-03-07')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2022-03-07')
|
parseDate('2022-03-07')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueWithCurrencyEffect: 0
|
valueWithCurrencyEffect: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('19.86'),
|
grossPerformance: new Big('19.86'),
|
||||||
@ -182,6 +182,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('0')
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
|
||||||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
|
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
|
||||||
|
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
|
||||||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
@ -23,19 +23,27 @@ import { cloneDeep, first, last, sortBy } from 'lodash';
|
|||||||
export class TWRPortfolioCalculator extends PortfolioCalculator {
|
export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||||
protected calculateOverallPerformance(
|
protected calculateOverallPerformance(
|
||||||
positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
): CurrentPositions {
|
): PortfolioSnapshot {
|
||||||
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);
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
let netPerformance = new Big(0);
|
let netPerformance = new Big(0);
|
||||||
let netPerformanceWithCurrencyEffect = new Big(0);
|
let netPerformanceWithCurrencyEffect = new Big(0);
|
||||||
|
let totalFeesWithCurrencyEffect = new Big(0);
|
||||||
|
let totalInterestWithCurrencyEffect = new Big(0);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentWithCurrencyEffect = new Big(0);
|
let totalInvestmentWithCurrencyEffect = new Big(0);
|
||||||
let totalTimeWeightedInvestment = new Big(0);
|
let totalTimeWeightedInvestment = new Big(0);
|
||||||
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
||||||
|
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
|
if (currentPosition.fee) {
|
||||||
|
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
|
||||||
|
currentPosition.fee
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentPosition.valueInBaseCurrency) {
|
if (currentPosition.valueInBaseCurrency) {
|
||||||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||||
currentPosition.valueInBaseCurrency
|
currentPosition.valueInBaseCurrency
|
||||||
@ -101,6 +109,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
hasErrors,
|
hasErrors,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformanceWithCurrencyEffect,
|
netPerformanceWithCurrencyEffect,
|
||||||
|
totalFeesWithCurrencyEffect,
|
||||||
|
totalInterestWithCurrencyEffect,
|
||||||
totalInvestment,
|
totalInvestment,
|
||||||
totalInvestmentWithCurrencyEffect,
|
totalInvestmentWithCurrencyEffect,
|
||||||
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
||||||
@ -178,6 +188,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
|
|
||||||
let totalDividend = new Big(0);
|
let totalDividend = new Big(0);
|
||||||
let totalDividendInBaseCurrency = new Big(0);
|
let totalDividendInBaseCurrency = new Big(0);
|
||||||
|
let totalInterest = new Big(0);
|
||||||
|
let totalInterestInBaseCurrency = new Big(0);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentFromBuyTransactions = new Big(0);
|
let totalInvestmentFromBuyTransactions = new Big(0);
|
||||||
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
|
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
|
||||||
@ -198,6 +210,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues: {},
|
currentValues: {},
|
||||||
currentValuesWithCurrencyEffect: {},
|
currentValuesWithCurrencyEffect: {},
|
||||||
|
feesWithCurrencyEffect: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -220,6 +233,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||||
totalDividend: new Big(0),
|
totalDividend: new Big(0),
|
||||||
totalDividendInBaseCurrency: new Big(0),
|
totalDividendInBaseCurrency: new Big(0),
|
||||||
|
totalInterest: new Big(0),
|
||||||
|
totalInterestInBaseCurrency: new Big(0),
|
||||||
totalInvestment: new Big(0),
|
totalInvestment: new Big(0),
|
||||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||||
};
|
};
|
||||||
@ -240,6 +255,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues: {},
|
currentValues: {},
|
||||||
currentValuesWithCurrencyEffect: {},
|
currentValuesWithCurrencyEffect: {},
|
||||||
|
feesWithCurrencyEffect: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -262,6 +278,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||||
totalDividend: new Big(0),
|
totalDividend: new Big(0),
|
||||||
totalDividendInBaseCurrency: new Big(0),
|
totalDividendInBaseCurrency: new Big(0),
|
||||||
|
totalInterest: new Big(0),
|
||||||
|
totalInterestInBaseCurrency: new Big(0),
|
||||||
totalInvestment: new Big(0),
|
totalInvestment: new Big(0),
|
||||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||||
};
|
};
|
||||||
@ -511,6 +529,13 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
|
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
|
||||||
dividend.mul(exchangeRateAtOrderDate ?? 1)
|
dividend.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
);
|
);
|
||||||
|
} else if (order.type === 'INTEREST') {
|
||||||
|
const interest = order.quantity.mul(order.unitPrice);
|
||||||
|
|
||||||
|
totalInterest = totalInterest.plus(interest);
|
||||||
|
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
|
||||||
|
interest.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
||||||
@ -808,6 +833,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues,
|
currentValues,
|
||||||
currentValuesWithCurrencyEffect,
|
currentValuesWithCurrencyEffect,
|
||||||
|
feesWithCurrencyEffect,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
grossPerformancePercentageWithCurrencyEffect,
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
initialValue,
|
initialValue,
|
||||||
@ -823,6 +849,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentValuesWithCurrencyEffect,
|
timeWeightedInvestmentValuesWithCurrencyEffect,
|
||||||
totalDividend,
|
totalDividend,
|
||||||
totalDividendInBaseCurrency,
|
totalDividendInBaseCurrency,
|
||||||
|
totalInterest,
|
||||||
|
totalInterestInBaseCurrency,
|
||||||
totalInvestment,
|
totalInvestment,
|
||||||
totalInvestmentWithCurrencyEffect,
|
totalInvestmentWithCurrencyEffect,
|
||||||
grossPerformance: totalGrossPerformance,
|
grossPerformance: totalGrossPerformance,
|
||||||
|
@ -2,7 +2,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
|||||||
|
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
export interface CurrentPositions extends ResponseError {
|
export interface PortfolioSnapshot extends ResponseError {
|
||||||
currentValueInBaseCurrency: Big;
|
currentValueInBaseCurrency: Big;
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformanceWithCurrencyEffect: Big;
|
grossPerformanceWithCurrencyEffect: Big;
|
||||||
@ -15,6 +15,8 @@ export interface CurrentPositions extends ResponseError {
|
|||||||
netPerformancePercentage: Big;
|
netPerformancePercentage: Big;
|
||||||
netPerformancePercentageWithCurrencyEffect: Big;
|
netPerformancePercentageWithCurrencyEffect: Big;
|
||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
|
totalFeesWithCurrencyEffect: Big;
|
||||||
|
totalInterestWithCurrencyEffect: Big;
|
||||||
totalInvestment: Big;
|
totalInvestment: Big;
|
||||||
totalInvestmentWithCurrencyEffect: Big;
|
totalInvestmentWithCurrencyEffect: Big;
|
||||||
}
|
}
|
@ -1,6 +1,10 @@
|
|||||||
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
||||||
|
|
||||||
export interface TransactionPoint {
|
export interface TransactionPoint {
|
||||||
date: string;
|
date: string;
|
||||||
|
fees: Big;
|
||||||
|
interest: Big;
|
||||||
items: TransactionPointSymbol[];
|
items: TransactionPointSymbol[];
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,8 @@ export class PortfolioController {
|
|||||||
dateRange,
|
dateRange,
|
||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
withLiabilities,
|
// TODO
|
||||||
|
// withLiabilities,
|
||||||
withMarkets,
|
withMarkets,
|
||||||
userId: this.request.user.id,
|
userId: this.request.user.id,
|
||||||
withSummary: true
|
withSummary: true
|
||||||
@ -389,11 +390,9 @@ export class PortfolioController {
|
|||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
|
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
|
||||||
@Query('withItems') withItemsParam = 'false'
|
|
||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
const withItems = withItemsParam === 'true';
|
|
||||||
|
|
||||||
const hasReadRestrictedAccessPermission =
|
const hasReadRestrictedAccessPermission =
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
this.userService.hasReadRestrictedAccessPermission({
|
||||||
@ -412,7 +411,6 @@ export class PortfolioController {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
withItems,
|
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,12 +23,7 @@ import {
|
|||||||
EMERGENCY_FUND_TAG_ID,
|
EMERGENCY_FUND_TAG_ID,
|
||||||
UNKNOWN_KEY
|
UNKNOWN_KEY
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
getAllActivityTypes,
|
|
||||||
getSum,
|
|
||||||
parseDate
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
@ -78,6 +73,7 @@ import {
|
|||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
||||||
|
|
||||||
|
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
PortfolioCalculatorFactory
|
PortfolioCalculatorFactory
|
||||||
@ -349,19 +345,8 @@ export class PortfolioService {
|
|||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
|
|
||||||
let types = getAllActivityTypes().filter((activityType) => {
|
|
||||||
return activityType !== 'FEE';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (withLiabilities === false) {
|
|
||||||
types = types.filter((activityType) => {
|
|
||||||
return activityType !== 'LIABILITY';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
types,
|
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts
|
withExcludedAccounts
|
||||||
@ -369,16 +354,13 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency
|
currency: userCurrency
|
||||||
});
|
});
|
||||||
|
|
||||||
const { startDate } = getInterval(
|
const { currentValueInBaseCurrency, hasErrors, positions } =
|
||||||
dateRange,
|
await portfolioCalculator.getSnapshot();
|
||||||
portfolioCalculator.getStartDate()
|
|
||||||
);
|
|
||||||
const currentPositions =
|
|
||||||
await portfolioCalculator.getCurrentPositions(startDate);
|
|
||||||
|
|
||||||
const cashDetails = await this.accountService.getCashDetails({
|
const cashDetails = await this.accountService.getCashDetails({
|
||||||
filters,
|
filters,
|
||||||
@ -388,10 +370,9 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const holdings: PortfolioDetails['holdings'] = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
|
|
||||||
const totalValueInBaseCurrency =
|
const totalValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||||
currentPositions.currentValueInBaseCurrency.plus(
|
cashDetails.balanceInBaseCurrency
|
||||||
cashDetails.balanceInBaseCurrency
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const isFilteredByAccount =
|
const isFilteredByAccount =
|
||||||
filters?.some(({ type }) => {
|
filters?.some(({ type }) => {
|
||||||
@ -409,7 +390,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
let filteredValueInBaseCurrency = isFilteredByAccount
|
let filteredValueInBaseCurrency = isFilteredByAccount
|
||||||
? totalValueInBaseCurrency
|
? totalValueInBaseCurrency
|
||||||
: currentPositions.currentValueInBaseCurrency;
|
: currentValueInBaseCurrency;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
filters?.length === 0 ||
|
filters?.length === 0 ||
|
||||||
@ -422,14 +403,12 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataGatheringItems = currentPositions.positions.map(
|
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
|
||||||
({ dataSource, symbol }) => {
|
return {
|
||||||
return {
|
dataSource,
|
||||||
dataSource,
|
symbol
|
||||||
symbol
|
};
|
||||||
};
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||||
@ -442,7 +421,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||||
for (const position of currentPositions.positions) {
|
for (const position of positions) {
|
||||||
portfolioItemsNow[position.symbol] = position;
|
portfolioItemsNow[position.symbol] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +444,7 @@ export class PortfolioService {
|
|||||||
tags,
|
tags,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
valueInBaseCurrency
|
valueInBaseCurrency
|
||||||
} of currentPositions.positions) {
|
} of positions) {
|
||||||
if (isFilteredByClosedHoldings === true) {
|
if (isFilteredByClosedHoldings === true) {
|
||||||
if (!quantity.eq(0)) {
|
if (!quantity.eq(0)) {
|
||||||
// Ignore positions with a quantity
|
// Ignore positions with a quantity
|
||||||
@ -593,6 +572,7 @@ export class PortfolioService {
|
|||||||
filteredValueInBaseCurrency,
|
filteredValueInBaseCurrency,
|
||||||
holdings,
|
holdings,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
portfolioCalculator,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
||||||
@ -605,10 +585,10 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
|
hasErrors,
|
||||||
holdings,
|
holdings,
|
||||||
platforms,
|
platforms,
|
||||||
summary,
|
summary
|
||||||
hasErrors: currentPositions.hasErrors
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,10 +661,9 @@ export class PortfolioService {
|
|||||||
const portfolioStart = portfolioCalculator.getStartDate();
|
const portfolioStart = portfolioCalculator.getStartDate();
|
||||||
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||||
|
|
||||||
const currentPositions =
|
const { positions } = await portfolioCalculator.getSnapshot();
|
||||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
|
||||||
|
|
||||||
const position = currentPositions.positions.find(({ symbol }) => {
|
const position = positions.find(({ symbol }) => {
|
||||||
return symbol === aSymbol;
|
return symbol === aSymbol;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -916,13 +895,12 @@ export class PortfolioService {
|
|||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
const { endDate } = getInterval(dateRange);
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
endDate,
|
endDate,
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
types: ['BUY', 'SELL'],
|
|
||||||
userCurrency: this.getUserCurrency()
|
userCurrency: this.getUserCurrency()
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -935,16 +913,14 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency
|
currency: this.request.user.Settings.settings.baseCurrency
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
let { hasErrors, positions } = await portfolioCalculator.getSnapshot();
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
);
|
|
||||||
|
|
||||||
let positions = currentPositions.positions.filter(({ quantity }) => {
|
positions = positions.filter(({ quantity }) => {
|
||||||
return !quantity.eq(0);
|
return !quantity.eq(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -983,7 +959,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors,
|
hasErrors,
|
||||||
positions: positions.map(
|
positions: positions.map(
|
||||||
({
|
({
|
||||||
averagePrice,
|
averagePrice,
|
||||||
@ -1050,15 +1026,13 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false,
|
withExcludedAccounts = false
|
||||||
withItems = false
|
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
withItems?: boolean;
|
|
||||||
}): Promise<PortfolioPerformanceResponse> {
|
}): Promise<PortfolioPerformanceResponse> {
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
@ -1096,8 +1070,7 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts
|
||||||
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
|
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
|
||||||
@ -1123,6 +1096,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency
|
currency: userCurrency
|
||||||
});
|
});
|
||||||
@ -1140,7 +1114,7 @@ export class PortfolioService {
|
|||||||
netPerformancePercentageWithCurrencyEffect,
|
netPerformancePercentageWithCurrencyEffect,
|
||||||
netPerformanceWithCurrencyEffect,
|
netPerformanceWithCurrencyEffect,
|
||||||
totalInvestment
|
totalInvestment
|
||||||
} = await portfolioCalculator.getCurrentPositions(startDate, endDate);
|
} = await portfolioCalculator.getSnapshot();
|
||||||
|
|
||||||
let currentNetPerformance = netPerformance;
|
let currentNetPerformance = netPerformance;
|
||||||
|
|
||||||
@ -1231,8 +1205,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId
|
||||||
types: ['BUY', 'SELL']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
@ -1241,13 +1214,10 @@ export class PortfolioService {
|
|||||||
currency: this.request.user.Settings.settings.baseCurrency
|
currency: this.request.user.Settings.settings.baseCurrency
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
|
||||||
portfolioCalculator.getStartDate()
|
await portfolioCalculator.getSnapshot();
|
||||||
);
|
|
||||||
|
|
||||||
const positions = currentPositions.positions.filter(
|
positions = positions.filter((item) => !item.quantity.eq(0));
|
||||||
(item) => !item.quantity.eq(0)
|
|
||||||
);
|
|
||||||
|
|
||||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||||
|
|
||||||
@ -1309,8 +1279,8 @@ export class PortfolioService {
|
|||||||
[
|
[
|
||||||
new FeeRatioInitialInvestment(
|
new FeeRatioInitialInvestment(
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
currentPositions.totalInvestment.toNumber(),
|
totalInvestment.toNumber(),
|
||||||
this.getFees({ activities, userCurrency }).toNumber()
|
totalFeesWithCurrencyEffect.toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
userSettings
|
userSettings
|
||||||
@ -1454,30 +1424,6 @@ export class PortfolioService {
|
|||||||
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFees({
|
|
||||||
activities,
|
|
||||||
userCurrency
|
|
||||||
}: {
|
|
||||||
activities: Activity[];
|
|
||||||
userCurrency: string;
|
|
||||||
}) {
|
|
||||||
return getSum(
|
|
||||||
activities
|
|
||||||
.filter(({ isDraft }) => {
|
|
||||||
return isDraft === false;
|
|
||||||
})
|
|
||||||
.map(({ fee, SymbolProfile }) => {
|
|
||||||
return new Big(
|
|
||||||
this.exchangeRateDataService.toCurrency(
|
|
||||||
fee,
|
|
||||||
SymbolProfile.currency,
|
|
||||||
userCurrency
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInitialCashPosition({
|
private getInitialCashPosition({
|
||||||
balance,
|
balance,
|
||||||
currency
|
currency
|
||||||
@ -1623,6 +1569,7 @@ export class PortfolioService {
|
|||||||
filteredValueInBaseCurrency,
|
filteredValueInBaseCurrency,
|
||||||
holdings,
|
holdings,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
portfolioCalculator,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
@ -1631,6 +1578,7 @@ export class PortfolioService {
|
|||||||
filteredValueInBaseCurrency: Big;
|
filteredValueInBaseCurrency: Big;
|
||||||
holdings: PortfolioDetails['holdings'];
|
holdings: PortfolioDetails['holdings'];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
|
portfolioCalculator: PortfolioCalculator;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<PortfolioSummary> {
|
}): Promise<PortfolioSummary> {
|
||||||
@ -1659,17 +1607,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dividendInBaseCurrency = getSum(
|
const dividendInBaseCurrency =
|
||||||
(
|
await portfolioCalculator.getDividendInBaseCurrency();
|
||||||
await this.getDividends({
|
|
||||||
activities: activities.filter(({ type }) => {
|
|
||||||
return type === 'DIVIDEND';
|
|
||||||
})
|
|
||||||
})
|
|
||||||
).map(({ investment }) => {
|
|
||||||
return new Big(investment);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
Math.max(
|
Math.max(
|
||||||
@ -1678,15 +1617,13 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
const fees = await portfolioCalculator.getFeesInBaseCurrency();
|
||||||
const firstOrderDate = activities[0]?.date;
|
|
||||||
|
|
||||||
const interest = this.getSumOfActivityType({
|
const firstOrderDate = portfolioCalculator.getStartDate();
|
||||||
activities,
|
|
||||||
userCurrency,
|
|
||||||
activityType: 'INTEREST'
|
|
||||||
}).toNumber();
|
|
||||||
|
|
||||||
|
const interest = await portfolioCalculator.getInterestInBaseCurrency();
|
||||||
|
|
||||||
|
// TODO: Move to portfolio calculator
|
||||||
const items = getSum(
|
const items = getSum(
|
||||||
Object.keys(holdings)
|
Object.keys(holdings)
|
||||||
.filter((symbol) => {
|
.filter((symbol) => {
|
||||||
@ -1701,6 +1638,7 @@ export class PortfolioService {
|
|||||||
})
|
})
|
||||||
).toNumber();
|
).toNumber();
|
||||||
|
|
||||||
|
// TODO: Move to portfolio calculator
|
||||||
const liabilities = getSum(
|
const liabilities = getSum(
|
||||||
Object.keys(holdings)
|
Object.keys(holdings)
|
||||||
.filter((symbol) => {
|
.filter((symbol) => {
|
||||||
@ -1791,9 +1729,7 @@ export class PortfolioService {
|
|||||||
annualizedPerformancePercentWithCurrencyEffect,
|
annualizedPerformancePercentWithCurrencyEffect,
|
||||||
cash,
|
cash,
|
||||||
excludedAccountsAndActivities,
|
excludedAccountsAndActivities,
|
||||||
fees,
|
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
interest,
|
|
||||||
items,
|
items,
|
||||||
liabilities,
|
liabilities,
|
||||||
totalBuy,
|
totalBuy,
|
||||||
@ -1807,6 +1743,7 @@ export class PortfolioService {
|
|||||||
.toNumber(),
|
.toNumber(),
|
||||||
total: emergencyFund.toNumber()
|
total: emergencyFund.toNumber()
|
||||||
},
|
},
|
||||||
|
fees: fees.toNumber(),
|
||||||
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
||||||
filteredValueInPercentage: netWorth
|
filteredValueInPercentage: netWorth
|
||||||
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||||
@ -1814,6 +1751,7 @@ export class PortfolioService {
|
|||||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
|
interest: interest.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return type === 'BUY' || type === 'SELL';
|
||||||
}).length,
|
}).length,
|
||||||
|
@ -37,36 +37,48 @@ export function getInterval(
|
|||||||
aDateRange: DateRange,
|
aDateRange: DateRange,
|
||||||
portfolioStart = new Date(0)
|
portfolioStart = new Date(0)
|
||||||
) {
|
) {
|
||||||
let endDate = endOfDay(new Date());
|
let endDate = endOfDay(new Date(Date.now()));
|
||||||
let startDate = portfolioStart;
|
let startDate = portfolioStart;
|
||||||
|
|
||||||
switch (aDateRange) {
|
switch (aDateRange) {
|
||||||
case '1d':
|
case '1d':
|
||||||
startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
|
startDate = max([
|
||||||
|
startDate,
|
||||||
|
subDays(resetHours(new Date(Date.now())), 1)
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
case 'mtd':
|
case 'mtd':
|
||||||
startDate = max([
|
startDate = max([
|
||||||
startDate,
|
startDate,
|
||||||
subDays(startOfMonth(resetHours(new Date())), 1)
|
subDays(startOfMonth(resetHours(new Date(Date.now()))), 1)
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'wtd':
|
case 'wtd':
|
||||||
startDate = max([
|
startDate = max([
|
||||||
startDate,
|
startDate,
|
||||||
subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1)
|
subDays(
|
||||||
|
startOfWeek(resetHours(new Date(Date.now())), { weekStartsOn: 1 }),
|
||||||
|
1
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'ytd':
|
case 'ytd':
|
||||||
startDate = max([
|
startDate = max([
|
||||||
startDate,
|
startDate,
|
||||||
subDays(startOfYear(resetHours(new Date())), 1)
|
subDays(startOfYear(resetHours(new Date(Date.now()))), 1)
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case '1y':
|
case '1y':
|
||||||
startDate = max([startDate, subYears(resetHours(new Date()), 1)]);
|
startDate = max([
|
||||||
|
startDate,
|
||||||
|
subYears(resetHours(new Date(Date.now())), 1)
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
case '5y':
|
case '5y':
|
||||||
startDate = max([startDate, subYears(resetHours(new Date()), 5)]);
|
startDate = max([
|
||||||
|
startDate,
|
||||||
|
subYears(resetHours(new Date(Date.now())), 5)
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
case 'max':
|
case 'max':
|
||||||
break;
|
break;
|
||||||
|
@ -7,6 +7,7 @@ export interface SymbolMetrics {
|
|||||||
currentValuesWithCurrencyEffect: {
|
currentValuesWithCurrencyEffect: {
|
||||||
[date: string]: Big;
|
[date: string]: Big;
|
||||||
};
|
};
|
||||||
|
feesWithCurrencyEffect: Big;
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
grossPerformancePercentageWithCurrencyEffect: Big;
|
grossPerformancePercentageWithCurrencyEffect: Big;
|
||||||
@ -41,6 +42,8 @@ export interface SymbolMetrics {
|
|||||||
timeWeightedInvestmentWithCurrencyEffect: Big;
|
timeWeightedInvestmentWithCurrencyEffect: Big;
|
||||||
totalDividend: Big;
|
totalDividend: Big;
|
||||||
totalDividendInBaseCurrency: Big;
|
totalDividendInBaseCurrency: Big;
|
||||||
|
totalInterest: Big;
|
||||||
|
totalInterestInBaseCurrency: Big;
|
||||||
totalInvestment: Big;
|
totalInvestment: Big;
|
||||||
totalInvestmentWithCurrencyEffect: Big;
|
totalInvestmentWithCurrencyEffect: Big;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user