Feature/move dividend fee and interest calculation to portfolio calculator (#3267)

* Move dividend, feee and interest calculation to portfolio calculator

* Update changelog
pull/3271/head
Thomas Kaul 6 months ago committed by GitHub
parent b31bbbe2d1
commit 6c57609db8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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/),
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
### Added

@ -1,5 +1,5 @@
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 {
SymbolMetrics,
TimelinePosition,
@ -9,7 +9,7 @@ import {
export class MWRPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance(
positions: TimelinePosition[]
): CurrentPositions {
): PortfolioSnapshot {
throw new Error('Method not implemented.');
}

@ -1,6 +1,7 @@
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 { DateRange } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
@ -23,17 +24,20 @@ export class PortfolioCalculatorFactory {
public createCalculator({
activities,
calculationType,
currency
currency,
dateRange = 'max'
}: {
activities: Activity[];
calculationType: PerformanceCalculationType;
currency: string;
dateRange?: DateRange;
}): PortfolioCalculator {
switch (calculationType) {
case PerformanceCalculationType.MWR:
return new MWRPortfolioCalculator({
activities,
currency,
dateRange,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService
});
@ -42,6 +46,7 @@ export class PortfolioCalculatorFactory {
activities,
currency,
currentRateService: this.currentRateService,
dateRange,
exchangeRateDataService: this.exchangeRateDataService
});
default:

@ -1,7 +1,7 @@
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 { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.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 {
@ -11,7 +11,12 @@ import {
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
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 {
DataProviderInfo,
HistoricalDataItem,
@ -44,18 +49,24 @@ export abstract class PortfolioCalculator {
private currency: string;
private currentRateService: CurrentRateService;
private dataProviderInfos: DataProviderInfo[];
private endDate: Date;
private exchangeRateDataService: ExchangeRateDataService;
private snapshot: PortfolioSnapshot;
private snapshotPromise: Promise<void>;
private startDate: Date;
private transactionPoints: TransactionPoint[];
public constructor({
activities,
currency,
currentRateService,
dateRange,
exchangeRateDataService
}: {
activities: Activity[];
currency: string;
currentRateService: CurrentRateService;
dateRange: DateRange;
exchangeRateDataService: ExchangeRateDataService;
}) {
this.currency = currency;
@ -79,12 +90,270 @@ export abstract class PortfolioCalculator {
return a.date?.localeCompare(b.date);
});
const { endDate, startDate } = getInterval(dateRange);
this.endDate = endDate;
this.startDate = startDate;
this.computeTransactionPoints();
this.snapshotPromise = this.initialize();
}
protected abstract calculateOverallPerformance(
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({
dateRange = 'max',
@ -380,256 +649,30 @@ 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));
public getDataProviderInfos() {
return this.dataProviderInfos;
}
// 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()));
public async getDividendInBaseCurrency() {
await this.snapshotPromise;
dates = uniq(
dates.map((date) => {
return date.getTime();
})
)
.map((timestamp) => {
return new Date(timestamp);
return getSum(
this.snapshot.positions.map(({ dividendInBaseCurrency }) => {
return dividendInBaseCurrency;
})
.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);
public async getFeesInBaseCurrency() {
await this.snapshotPromise;
return {
...overall,
errors,
positions,
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
};
return this.snapshot.totalFeesWithCurrencyEffect;
}
public getDataProviderInfos() {
return this.dataProviderInfos;
public async getInterestInBaseCurrency() {
await this.snapshotPromise;
return this.snapshot.totalInterestWithCurrencyEffect;
}
public getInvestments(): { date: string; investment: Big }[] {
@ -672,6 +715,12 @@ export abstract class PortfolioCalculator {
}));
}
public async getSnapshot() {
await this.snapshotPromise;
return this.snapshot;
}
public getStartDate() {
return this.transactionPoints.length > 0
? parseDate(this.transactionPoints[0].date)
@ -718,6 +767,13 @@ export abstract class PortfolioCalculator {
type,
unitPrice
} of this.orders) {
if (
// TODO
['ITEM', 'LIABILITY'].includes(type)
) {
continue;
}
let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
@ -790,18 +846,39 @@ export abstract class PortfolioCalculator {
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) {
lastTransactionPoint = {
date,
fees,
interest,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees);
lastTransactionPoint.interest =
lastTransactionPoint.interest.plus(interest);
lastTransactionPoint.items = newItems;
}
lastDate = date;
}
}
private async initialize() {
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate);
}
}

@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
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[] = [
{
...activityDummyData,
@ -100,15 +104,11 @@ describe('PortfolioCalculator', () => {
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-22')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-22')
);
@ -121,7 +121,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('-12.6'),
@ -173,6 +173,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});

@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
it.only('with BALN.SW buy and sell', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => {
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-22')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-22')
);
@ -106,7 +106,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('-12.6'),
@ -156,6 +156,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});

@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
it.only('with BALN.SW buy', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
@ -70,15 +74,11 @@ describe('PortfolioCalculator', () => {
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-30')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-30')
);
@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('297.8'),
errors: [],
grossPerformance: new Big('24.6'),
@ -141,6 +141,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('297.8')
}
],
totalFeesWithCurrencyEffect: new Big('1.55'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('273.2'),
totalInvestmentWithCurrencyEffect: new Big('273.2')
});

@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
it.only('with BTCUSD buy and sell partially', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime());
const activities: Activity[] = [
{
...activityDummyData,
@ -98,15 +102,11 @@ describe('PortfolioCalculator', () => {
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2015-01-01')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2015-01-01')
);
@ -119,7 +119,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('13298.425356'),
errors: [],
grossPerformance: new Big('27172.74'),
@ -175,6 +175,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('13298.425356')
}
],
totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('320.43'),
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', () => {
it.only('with GOOGL buy', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const activities: Activity[] = [
{
...activityDummyData,
@ -83,15 +87,11 @@ describe('PortfolioCalculator', () => {
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2023-01-03')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2023-01-03')
);
@ -104,7 +104,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('103.10483'),
errors: [],
grossPerformance: new Big('27.33'),
@ -154,6 +154,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('103.10483')
}
],
totalFeesWithCurrencyEffect: new Big('1'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('89.12'),
totalInvestmentWithCurrencyEffect: new Big('82.329056')
});

@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
it.only('with MSFT buy', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const activities: Activity[] = [
{
...activityDummyData,
@ -98,17 +102,13 @@ describe('PortfolioCalculator', () => {
currency: 'USD'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2023-07-10')
);
spy.mockRestore();
expect(currentPositions).toMatchObject({
expect(portfolioSnapshot).toMatchObject({
errors: [],
hasErrors: false,
positions: [
@ -130,6 +130,8 @@ describe('PortfolioCalculator', () => {
transactionCount: 2
}
],
totalFeesWithCurrencyEffect: new Big('19'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('298.58'),
totalInvestmentWithCurrencyEffect: new Big('298.58')
});

@ -42,22 +42,22 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
it('with no orders', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const portfolioCalculator = factory.createCalculator({
activities: [],
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const start = subDays(new Date(Date.now()), 10);
const chartData = await portfolioCalculator.getChartData({ start });
const currentPositions =
await portfolioCalculator.getCurrentPositions(start);
const portfolioSnapshot =
await portfolioCalculator.computeSnapshot(start);
const investments = portfolioCalculator.getInvestments();
@ -68,7 +68,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
@ -80,6 +80,8 @@ describe('PortfolioCalculator', () => {
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)
});

@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
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[] = [
{
...activityDummyData,
@ -84,15 +88,12 @@ describe('PortfolioCalculator', () => {
calculationType: PerformanceCalculationType.TWR,
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2022-03-07')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2022-03-07')
);
@ -105,7 +106,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('87.8'),
errors: [],
grossPerformance: new Big('21.93'),
@ -157,6 +158,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('87.8')
}
],
totalFeesWithCurrencyEffect: new Big('4.25'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('75.80'),
totalInvestmentWithCurrencyEffect: new Big('75.80')
});

@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
describe('get current positions', () => {
it.only('with NOVN.SW buy and sell', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime());
const activities: Activity[] = [
{
...activityDummyData,
@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => {
currency: 'CHF'
});
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2022-03-07')
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2022-03-07')
);
@ -132,7 +132,7 @@ describe('PortfolioCalculator', () => {
valueWithCurrencyEffect: 0
});
expect(currentPositions).toEqual({
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('19.86'),
@ -182,6 +182,8 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});

@ -1,6 +1,6 @@
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 { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
@ -23,19 +23,27 @@ import { cloneDeep, first, last, sortBy } from 'lodash';
export class TWRPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance(
positions: TimelinePosition[]
): CurrentPositions {
): PortfolioSnapshot {
let currentValueInBaseCurrency = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false;
let netPerformance = new Big(0);
let netPerformanceWithCurrencyEffect = new Big(0);
let totalFeesWithCurrencyEffect = new Big(0);
let totalInterestWithCurrencyEffect = new Big(0);
let totalInvestment = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0);
let totalTimeWeightedInvestment = new Big(0);
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) {
if (currentPosition.fee) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.fee
);
}
if (currentPosition.valueInBaseCurrency) {
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
currentPosition.valueInBaseCurrency
@ -101,6 +109,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
hasErrors,
netPerformance,
netPerformanceWithCurrencyEffect,
totalFeesWithCurrencyEffect,
totalInterestWithCurrencyEffect,
totalInvestment,
totalInvestmentWithCurrencyEffect,
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
@ -178,6 +188,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
let totalDividend = new Big(0);
let totalDividendInBaseCurrency = new Big(0);
let totalInterest = new Big(0);
let totalInterestInBaseCurrency = new Big(0);
let totalInvestment = new Big(0);
let totalInvestmentFromBuyTransactions = new Big(0);
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
@ -198,6 +210,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
feesWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -220,6 +233,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalDividend: new Big(0),
totalDividendInBaseCurrency: new Big(0),
totalInterest: new Big(0),
totalInterestInBaseCurrency: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0)
};
@ -240,6 +255,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
feesWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -262,6 +278,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalDividend: new Big(0),
totalDividendInBaseCurrency: new Big(0),
totalInterest: new Big(0),
totalInterestInBaseCurrency: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0)
};
@ -511,6 +529,13 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
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);
@ -808,6 +833,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return {
currentValues,
currentValuesWithCurrencyEffect,
feesWithCurrencyEffect,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
initialValue,
@ -823,6 +849,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
timeWeightedInvestmentValuesWithCurrencyEffect,
totalDividend,
totalDividendInBaseCurrency,
totalInterest,
totalInterestInBaseCurrency,
totalInvestment,
totalInvestmentWithCurrencyEffect,
grossPerformance: totalGrossPerformance,

@ -2,7 +2,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import { Big } from 'big.js';
export interface CurrentPositions extends ResponseError {
export interface PortfolioSnapshot extends ResponseError {
currentValueInBaseCurrency: Big;
grossPerformance: Big;
grossPerformanceWithCurrencyEffect: Big;
@ -15,6 +15,8 @@ export interface CurrentPositions extends ResponseError {
netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big;
positions: TimelinePosition[];
totalFeesWithCurrencyEffect: Big;
totalInterestWithCurrencyEffect: Big;
totalInvestment: Big;
totalInvestmentWithCurrencyEffect: Big;
}

@ -1,6 +1,10 @@
import { Big } from 'big.js';
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
export interface TransactionPoint {
date: string;
fees: Big;
interest: Big;
items: TransactionPointSymbol[];
}

@ -107,7 +107,8 @@ export class PortfolioController {
dateRange,
filters,
impersonationId,
withLiabilities,
// TODO
// withLiabilities,
withMarkets,
userId: this.request.user.id,
withSummary: true
@ -389,11 +390,9 @@ export class PortfolioController {
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
@Query('withItems') withItemsParam = 'false'
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
): Promise<PortfolioPerformanceResponse> {
const withExcludedAccounts = withExcludedAccountsParam === 'true';
const withItems = withItemsParam === 'true';
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
@ -412,7 +411,6 @@ export class PortfolioController {
filters,
impersonationId,
withExcludedAccounts,
withItems,
userId: this.request.user.id
});

@ -23,12 +23,7 @@ import {
EMERGENCY_FUND_TAG_ID,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getAllActivityTypes,
getSum,
parseDate
} from '@ghostfolio/common/helper';
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
import {
Accounts,
EnhancedSymbolProfile,
@ -78,6 +73,7 @@ import {
} from 'date-fns';
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator';
import {
PerformanceCalculationType,
PortfolioCalculatorFactory
@ -349,19 +345,8 @@ export class PortfolioService {
(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({
filters,
types,
userCurrency,
userId,
withExcludedAccounts
@ -369,16 +354,13 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
dateRange,
calculationType: PerformanceCalculationType.TWR,
currency: userCurrency
});
const { startDate } = getInterval(
dateRange,
portfolioCalculator.getStartDate()
);
const currentPositions =
await portfolioCalculator.getCurrentPositions(startDate);
const { currentValueInBaseCurrency, hasErrors, positions } =
await portfolioCalculator.getSnapshot();
const cashDetails = await this.accountService.getCashDetails({
filters,
@ -388,10 +370,9 @@ export class PortfolioService {
const holdings: PortfolioDetails['holdings'] = {};
const totalValueInBaseCurrency =
currentPositions.currentValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency
);
const totalValueInBaseCurrency = currentValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency
);
const isFilteredByAccount =
filters?.some(({ type }) => {
@ -409,7 +390,7 @@ export class PortfolioService {
let filteredValueInBaseCurrency = isFilteredByAccount
? totalValueInBaseCurrency
: currentPositions.currentValueInBaseCurrency;
: currentValueInBaseCurrency;
if (
filters?.length === 0 ||
@ -422,14 +403,12 @@ export class PortfolioService {
);
}
const dataGatheringItems = currentPositions.positions.map(
({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
}
);
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
@ -442,7 +421,7 @@ export class PortfolioService {
}
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
for (const position of currentPositions.positions) {
for (const position of positions) {
portfolioItemsNow[position.symbol] = position;
}
@ -465,7 +444,7 @@ export class PortfolioService {
tags,
transactionCount,
valueInBaseCurrency
} of currentPositions.positions) {
} of positions) {
if (isFilteredByClosedHoldings === true) {
if (!quantity.eq(0)) {
// Ignore positions with a quantity
@ -593,6 +572,7 @@ export class PortfolioService {
filteredValueInBaseCurrency,
holdings,
impersonationId,
portfolioCalculator,
userCurrency,
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
@ -605,10 +585,10 @@ export class PortfolioService {
return {
accounts,
hasErrors,
holdings,
platforms,
summary,
hasErrors: currentPositions.hasErrors
summary
};
}
@ -681,10 +661,9 @@ export class PortfolioService {
const portfolioStart = portfolioCalculator.getStartDate();
const transactionPoints = portfolioCalculator.getTransactionPoints();
const currentPositions =
await portfolioCalculator.getCurrentPositions(portfolioStart);
const { positions } = await portfolioCalculator.getSnapshot();
const position = currentPositions.positions.find(({ symbol }) => {
const position = positions.find(({ symbol }) => {
return symbol === aSymbol;
});
@ -916,13 +895,12 @@ export class PortfolioService {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const { endDate, startDate } = getInterval(dateRange);
const { endDate } = getInterval(dateRange);
const { activities } = await this.orderService.getOrders({
endDate,
filters,
userId,
types: ['BUY', 'SELL'],
userCurrency: this.getUserCurrency()
});
@ -935,16 +913,14 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
dateRange,
calculationType: PerformanceCalculationType.TWR,
currency: this.request.user.Settings.settings.baseCurrency
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate,
endDate
);
let { hasErrors, positions } = await portfolioCalculator.getSnapshot();
let positions = currentPositions.positions.filter(({ quantity }) => {
positions = positions.filter(({ quantity }) => {
return !quantity.eq(0);
});
@ -983,7 +959,7 @@ export class PortfolioService {
}
return {
hasErrors: currentPositions.hasErrors,
hasErrors,
positions: positions.map(
({
averagePrice,
@ -1050,15 +1026,13 @@ export class PortfolioService {
filters,
impersonationId,
userId,
withExcludedAccounts = false,
withItems = false
withExcludedAccounts = false
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
withExcludedAccounts?: boolean;
withItems?: boolean;
}): Promise<PortfolioPerformanceResponse> {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
@ -1096,8 +1070,7 @@ export class PortfolioService {
filters,
userCurrency,
userId,
withExcludedAccounts,
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
withExcludedAccounts
});
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
@ -1123,6 +1096,7 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
dateRange,
calculationType: PerformanceCalculationType.TWR,
currency: userCurrency
});
@ -1140,7 +1114,7 @@ export class PortfolioService {
netPerformancePercentageWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
totalInvestment
} = await portfolioCalculator.getCurrentPositions(startDate, endDate);
} = await portfolioCalculator.getSnapshot();
let currentNetPerformance = netPerformance;
@ -1231,8 +1205,7 @@ export class PortfolioService {
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
types: ['BUY', 'SELL']
userId
});
const portfolioCalculator = this.calculatorFactory.createCalculator({
@ -1241,13 +1214,10 @@ export class PortfolioService {
currency: this.request.user.Settings.settings.baseCurrency
});
const currentPositions = await portfolioCalculator.getCurrentPositions(
portfolioCalculator.getStartDate()
);
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
await portfolioCalculator.getSnapshot();
const positions = currentPositions.positions.filter(
(item) => !item.quantity.eq(0)
);
positions = positions.filter((item) => !item.quantity.eq(0));
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
@ -1309,8 +1279,8 @@ export class PortfolioService {
[
new FeeRatioInitialInvestment(
this.exchangeRateDataService,
currentPositions.totalInvestment.toNumber(),
this.getFees({ activities, userCurrency }).toNumber()
totalInvestment.toNumber(),
totalFeesWithCurrencyEffect.toNumber()
)
],
userSettings
@ -1454,30 +1424,6 @@ export class PortfolioService {
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({
balance,
currency
@ -1623,6 +1569,7 @@ export class PortfolioService {
filteredValueInBaseCurrency,
holdings,
impersonationId,
portfolioCalculator,
userCurrency,
userId
}: {
@ -1631,6 +1578,7 @@ export class PortfolioService {
filteredValueInBaseCurrency: Big;
holdings: PortfolioDetails['holdings'];
impersonationId: string;
portfolioCalculator: PortfolioCalculator;
userCurrency: string;
userId: string;
}): Promise<PortfolioSummary> {
@ -1659,17 +1607,8 @@ export class PortfolioService {
}
}
const dividendInBaseCurrency = getSum(
(
await this.getDividends({
activities: activities.filter(({ type }) => {
return type === 'DIVIDEND';
})
})
).map(({ investment }) => {
return new Big(investment);
})
);
const dividendInBaseCurrency =
await portfolioCalculator.getDividendInBaseCurrency();
const emergencyFund = new Big(
Math.max(
@ -1678,15 +1617,13 @@ export class PortfolioService {
)
);
const fees = this.getFees({ activities, userCurrency }).toNumber();
const firstOrderDate = activities[0]?.date;
const fees = await portfolioCalculator.getFeesInBaseCurrency();
const interest = this.getSumOfActivityType({
activities,
userCurrency,
activityType: 'INTEREST'
}).toNumber();
const firstOrderDate = portfolioCalculator.getStartDate();
const interest = await portfolioCalculator.getInterestInBaseCurrency();
// TODO: Move to portfolio calculator
const items = getSum(
Object.keys(holdings)
.filter((symbol) => {
@ -1701,6 +1638,7 @@ export class PortfolioService {
})
).toNumber();
// TODO: Move to portfolio calculator
const liabilities = getSum(
Object.keys(holdings)
.filter((symbol) => {
@ -1791,9 +1729,7 @@ export class PortfolioService {
annualizedPerformancePercentWithCurrencyEffect,
cash,
excludedAccountsAndActivities,
fees,
firstOrderDate,
interest,
items,
liabilities,
totalBuy,
@ -1807,6 +1743,7 @@ export class PortfolioService {
.toNumber(),
total: emergencyFund.toNumber()
},
fees: fees.toNumber(),
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: netWorth
? filteredValueInBaseCurrency.div(netWorth).toNumber()
@ -1814,6 +1751,7 @@ export class PortfolioService {
fireWealth: new Big(performanceInformation.performance.currentValue)
.minus(emergencyFundPositionsValueInBaseCurrency)
.toNumber(),
interest: interest.toNumber(),
ordersCount: activities.filter(({ type }) => {
return type === 'BUY' || type === 'SELL';
}).length,

@ -37,36 +37,48 @@ export function getInterval(
aDateRange: DateRange,
portfolioStart = new Date(0)
) {
let endDate = endOfDay(new Date());
let endDate = endOfDay(new Date(Date.now()));
let startDate = portfolioStart;
switch (aDateRange) {
case '1d':
startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
startDate = max([
startDate,
subDays(resetHours(new Date(Date.now())), 1)
]);
break;
case 'mtd':
startDate = max([
startDate,
subDays(startOfMonth(resetHours(new Date())), 1)
subDays(startOfMonth(resetHours(new Date(Date.now()))), 1)
]);
break;
case 'wtd':
startDate = max([
startDate,
subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1)
subDays(
startOfWeek(resetHours(new Date(Date.now())), { weekStartsOn: 1 }),
1
)
]);
break;
case 'ytd':
startDate = max([
startDate,
subDays(startOfYear(resetHours(new Date())), 1)
subDays(startOfYear(resetHours(new Date(Date.now()))), 1)
]);
break;
case '1y':
startDate = max([startDate, subYears(resetHours(new Date()), 1)]);
startDate = max([
startDate,
subYears(resetHours(new Date(Date.now())), 1)
]);
break;
case '5y':
startDate = max([startDate, subYears(resetHours(new Date()), 5)]);
startDate = max([
startDate,
subYears(resetHours(new Date(Date.now())), 5)
]);
break;
case 'max':
break;

@ -7,6 +7,7 @@ export interface SymbolMetrics {
currentValuesWithCurrencyEffect: {
[date: string]: Big;
};
feesWithCurrencyEffect: Big;
grossPerformance: Big;
grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big;
@ -41,6 +42,8 @@ export interface SymbolMetrics {
timeWeightedInvestmentWithCurrencyEffect: Big;
totalDividend: Big;
totalDividendInBaseCurrency: Big;
totalInterest: Big;
totalInterestInBaseCurrency: Big;
totalInvestment: Big;
totalInvestmentWithCurrencyEffect: Big;
}

Loading…
Cancel
Save