Browse Source

Changes after merge

pull/5027/head
Daniel Devaud 10 months ago
parent
commit
5d54b8e349
  1. 2
      apps/api/src/app/admin/admin.service.ts
  2. 51
      apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts
  3. 10
      apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
  4. 90
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  5. 638
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

2
apps/api/src/app/admin/admin.service.ts

@ -536,6 +536,8 @@ export class AdminService {
})?._count ?? 0; })?._count ?? 0;
return { return {
activitiesCount,
date: dateOfFirstActivity,
dataSource, dataSource,
marketDataItemCount, marketDataItemCount,
symbol, symbol,

51
apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts

@ -2,16 +2,13 @@ import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
getFactor,
getInterval
} from '@ghostfolio/api/helper/portfolio.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { Filter, HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { Inject, Logger } from '@nestjs/common'; import { Inject, Logger } from '@nestjs/common';
@ -43,21 +40,19 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
configurationService, configurationService,
currency, currency,
currentRateService, currentRateService,
dateRange,
exchangeRateDataService, exchangeRateDataService,
redisCacheService, redisCacheService,
useCache, userId,
userId filters
}: { }: {
accountBalanceItems: HistoricalDataItem[]; accountBalanceItems: HistoricalDataItem[];
activities: Activity[]; activities: Activity[];
configurationService: ConfigurationService; configurationService: ConfigurationService;
currency: string; currency: string;
currentRateService: CurrentRateService; currentRateService: CurrentRateService;
dateRange: DateRange;
exchangeRateDataService: ExchangeRateDataService; exchangeRateDataService: ExchangeRateDataService;
redisCacheService: RedisCacheService; redisCacheService: RedisCacheService;
useCache: boolean; filters: Filter[];
userId: string; userId: string;
}, },
@Inject() @Inject()
@ -68,11 +63,10 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
activities, activities,
configurationService, configurationService,
currency, currency,
filters,
currentRateService, currentRateService,
dateRange,
exchangeRateDataService, exchangeRateDataService,
redisCacheService, redisCacheService,
useCache,
userId userId
}); });
} }
@ -87,23 +81,31 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
withDataDecimation?: boolean; withDataDecimation?: boolean;
withTimeWeightedReturn?: boolean; withTimeWeightedReturn?: boolean;
}): Promise<HistoricalDataItem[]> { }): Promise<HistoricalDataItem[]> {
const { endDate, startDate } = getInterval(dateRange, this.getStartDate()); const { endDate, startDate } = getIntervalFromDateRange(
dateRange,
this.getStartDate()
);
const daysInMarket = differenceInDays(endDate, startDate) + 1; const daysInMarket = differenceInDays(endDate, startDate) + 1;
const step = withDataDecimation const step = withDataDecimation
? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) ? Math.round(
daysInMarket /
Math.min(
daysInMarket,
this.configurationService.get('MAX_CHART_ITEMS')
)
)
: 1; : 1;
let item = super.getChartData({ let item = await super.getPerformance({
step,
end: endDate, end: endDate,
start: startDate start: startDate
}); });
if (!withTimeWeightedReturn) { if (!withTimeWeightedReturn) {
return item; return item.chart;
} else { } else {
let itemResult = await item; let itemResult = item.chart;
let dates = itemResult.map((item) => parseDate(item.date)); let dates = itemResult.map((item) => parseDate(item.date));
let timeWeighted = await this.getTimeWeightedChartData({ let timeWeighted = await this.getTimeWeightedChartData({
dates dates
@ -145,13 +147,14 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
const end = new Date(Date.now()); const end = new Date(Date.now());
const holdings = await this.getHoldings(orders, parseDate(start), end); const holdings = await this.getHoldings(orders, parseDate(start), end);
const marketMap = await this.currentRateService.getToday( const marketMap = await this.currentRateService.getValues({
this.mapToDataGatheringItems(orders) dataGatheringItems: this.mapToDataGatheringItems(orders),
); dateQuery: { in: [end] }
});
const endString = format(end, DATE_FORMAT); const endString = format(end, DATE_FORMAT);
let exchangeRates = await Promise.all( let exchangeRates = await Promise.all(
Object.keys(holdings[endString]).map(async (holding) => { Object.keys(holdings[endString]).map(async (holding) => {
let symbol = marketMap.find((m) => m.symbol === holding); let symbol = marketMap.values.find((m) => m.symbol === holding);
let symbolCurrency = this.getCurrencyFromActivities(orders, holding); let symbolCurrency = this.getCurrencyFromActivities(orders, holding);
let exchangeRate = await this.exchangeRateDataService.toCurrencyAtDate( let exchangeRate = await this.exchangeRateDataService.toCurrencyAtDate(
1, 1,
@ -175,7 +178,7 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
if (!holdings[endString][holding].toNumber()) { if (!holdings[endString][holding].toNumber()) {
return sum; return sum;
} }
let symbol = marketMap.find((m) => m.symbol === holding); let symbol = marketMap.values.find((m) => m.symbol === holding);
if (symbol?.marketPrice === undefined) { if (symbol?.marketPrice === undefined) {
Logger.warn( Logger.warn(

10
apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts

@ -66,12 +66,11 @@ export class PortfolioCalculatorFactory {
activities, activities,
currency, currency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
dateRange,
useCache,
userId, userId,
configurationService: this.configurationService, configurationService: this.configurationService,
exchangeRateDataService: this.exchangeRateDataService, exchangeRateDataService: this.exchangeRateDataService,
redisCacheService: this.redisCacheService redisCacheService: this.redisCacheService,
filters
}, },
this.orderservice this.orderservice
); );
@ -82,12 +81,11 @@ export class PortfolioCalculatorFactory {
activities, activities,
currency, currency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
dateRange,
useCache,
userId, userId,
configurationService: this.configurationService, configurationService: this.configurationService,
exchangeRateDataService: this.exchangeRateDataService, exchangeRateDataService: this.exchangeRateDataService,
redisCacheService: this.redisCacheService redisCacheService: this.redisCacheService,
filters
}, },
this.orderservice this.orderservice
); );

90
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -49,7 +49,7 @@ export abstract class PortfolioCalculator {
protected accountBalanceItems: HistoricalDataItem[]; protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[]; protected activities: PortfolioOrder[];
private configurationService: ConfigurationService; protected configurationService: ConfigurationService;
protected currency: string; protected currency: string;
protected currentRateService: CurrentRateService; protected currentRateService: CurrentRateService;
private dataProviderInfos: DataProviderInfo[]; private dataProviderInfos: DataProviderInfo[];
@ -379,15 +379,15 @@ export abstract class PortfolioCalculator {
dataSource: item.dataSource, dataSource: item.dataSource,
fee: item.fee, fee: item.fee,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? grossPerformance ?? null : null, grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors grossPerformancePercentage: !hasErrors
? grossPerformancePercentage ?? null ? (grossPerformancePercentage ?? null)
: null, : null,
grossPerformancePercentageWithCurrencyEffect: !hasErrors grossPerformancePercentageWithCurrencyEffect: !hasErrors
? grossPerformancePercentageWithCurrencyEffect ?? null ? (grossPerformancePercentageWithCurrencyEffect ?? null)
: null, : null,
grossPerformanceWithCurrencyEffect: !hasErrors grossPerformanceWithCurrencyEffect: !hasErrors
? grossPerformanceWithCurrencyEffect ?? null ? (grossPerformanceWithCurrencyEffect ?? null)
: null, : null,
investment: totalInvestment, investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
@ -395,15 +395,15 @@ export abstract class PortfolioCalculator {
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
marketPriceInBaseCurrency: marketPriceInBaseCurrency:
marketPriceInBaseCurrency?.toNumber() ?? null, marketPriceInBaseCurrency?.toNumber() ?? null,
netPerformance: !hasErrors ? netPerformance ?? null : null, netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors netPerformancePercentage: !hasErrors
? netPerformancePercentage ?? null ? (netPerformancePercentage ?? null)
: null, : null,
netPerformancePercentageWithCurrencyEffectMap: !hasErrors netPerformancePercentageWithCurrencyEffectMap: !hasErrors
? netPerformancePercentageWithCurrencyEffectMap ?? null ? (netPerformancePercentageWithCurrencyEffectMap ?? null)
: null, : null,
netPerformanceWithCurrencyEffectMap: !hasErrors netPerformanceWithCurrencyEffectMap: !hasErrors
? netPerformanceWithCurrencyEffectMap ?? null ? (netPerformanceWithCurrencyEffectMap ?? null)
: null, : null,
quantity: item.quantity, quantity: item.quantity,
symbol: item.symbol, symbol: item.symbol,
@ -491,8 +491,8 @@ export abstract class PortfolioCalculator {
return date === dateString; return date === dateString;
}).value }).value
) )
: accumulatedValuesByDate[lastDate] : (accumulatedValuesByDate[lastDate]
?.totalAccountBalanceWithCurrencyEffect ?? new Big(0), ?.totalAccountBalanceWithCurrencyEffect ?? new Big(0)),
totalCurrentValue: ( totalCurrentValue: (
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue), ).add(currentValue),
@ -873,74 +873,6 @@ export abstract class PortfolioCalculator {
return chartDateMap; return chartDateMap;
} }
private getChartDateMap({
endDate,
startDate,
step
}: {
endDate: Date;
startDate: Date;
step: number;
}) {
// Create a map of all relevant chart dates:
// 1. Add transaction point dates
let chartDateMap = this.transactionPoints.reduce((result, { date }) => {
result[date] = true;
return result;
}, {});
// 2. Add dates between transactions respecting the specified step size
for (let date of eachDayOfInterval(
{ end: endDate, start: startDate },
{ step }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
if (step > 1) {
// Reduce the step size of last 90 days
for (let date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 90) },
{ step: 3 }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
// Reduce the step size of last 30 days
for (let date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 30) },
{ step: 1 }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
}
// Make sure the end date is present
chartDateMap[format(endDate, DATE_FORMAT)] = true;
// Make sure some key dates are present
for (let dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
const { endDate: dateRangeEnd, startDate: dateRangeStart } =
getIntervalFromDateRange(dateRange);
if (
!isBefore(dateRangeStart, startDate) &&
!isAfter(dateRangeStart, endDate)
) {
chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
}
if (
!isBefore(dateRangeEnd, startDate) &&
!isAfter(dateRangeEnd, endDate)
) {
chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
}
}
return chartDateMap;
}
@LogPerformance @LogPerformance
protected computeTransactionPoints() { protected computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];

638
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -1,4 +1,3 @@
import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor';
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
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 { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
@ -12,7 +11,6 @@ import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { import {
addDays, addDays,
@ -25,7 +23,6 @@ import {
import { cloneDeep, first, last, sortBy } from 'lodash'; import { cloneDeep, first, last, sortBy } from 'lodash';
export class TWRPortfolioCalculator extends PortfolioCalculator { export class TWRPortfolioCalculator extends PortfolioCalculator {
@LogPerformance
protected calculateOverallPerformance( protected calculateOverallPerformance(
positions: TimelinePosition[] positions: TimelinePosition[]
): PortfolioSnapshot { ): PortfolioSnapshot {
@ -115,7 +112,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
}; };
} }
@LogPerformance
protected getSymbolMetrics({ protected getSymbolMetrics({
chartDateMap, chartDateMap,
dataSource, dataSource,
@ -167,7 +163,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
let totalAccountBalanceInBaseCurrency = new Big(0); let totalAccountBalanceInBaseCurrency = new Big(0);
let totalDividend = new Big(0); let totalDividend = new Big(0);
let totalStakeRewards = new Big(0);
let totalDividendInBaseCurrency = new Big(0); let totalDividendInBaseCurrency = new Big(0);
let totalInterest = new Big(0); let totalInterest = new Big(0);
let totalInterestInBaseCurrency = new Big(0); let totalInterestInBaseCurrency = new Big(0);
@ -195,7 +190,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return { return {
currentValues: {}, currentValues: {},
currentValuesWithCurrencyEffect: {}, currentValuesWithCurrencyEffect: {},
unitPrices: {},
feesWithCurrencyEffect: new Big(0), feesWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
@ -209,10 +203,11 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
investmentValuesWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
netPerformanceValuesPercentage: {},
unitPrices: {},
netPerformancePercentageWithCurrencyEffectMap: {}, netPerformancePercentageWithCurrencyEffectMap: {},
netPerformanceValues: {}, netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {}, netPerformanceValuesWithCurrencyEffect: {},
netPerformanceValuesPercentage: {},
netPerformanceWithCurrencyEffectMap: {}, netPerformanceWithCurrencyEffectMap: {},
timeWeightedInvestment: new Big(0), timeWeightedInvestment: new Big(0),
timeWeightedInvestmentValues: {}, timeWeightedInvestmentValues: {},
@ -247,7 +242,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return { return {
currentValues: {}, currentValues: {},
currentValuesWithCurrencyEffect: {}, currentValuesWithCurrencyEffect: {},
unitPrices: {},
feesWithCurrencyEffect: new Big(0), feesWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
@ -265,7 +259,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
netPerformanceWithCurrencyEffectMap: {}, netPerformanceWithCurrencyEffectMap: {},
netPerformanceValues: {}, netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {}, netPerformanceValuesWithCurrencyEffect: {},
netPerformanceValuesPercentage: {},
timeWeightedInvestment: new Big(0), timeWeightedInvestment: new Big(0),
timeWeightedInvestmentValues: {}, timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {}, timeWeightedInvestmentValuesWithCurrencyEffect: {},
@ -280,7 +273,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
totalLiabilities: new Big(0), totalLiabilities: new Big(0),
totalLiabilitiesInBaseCurrency: new Big(0), totalLiabilitiesInBaseCurrency: new Big(0),
totalValuables: new Big(0), totalValuables: new Big(0),
totalValuablesInBaseCurrency: new Big(0) totalValuablesInBaseCurrency: new Big(0),
netPerformanceValuesPercentage: {},
unitPrices: {}
}; };
} }
@ -381,316 +376,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
let sumOfTimeWeightedInvestments = new Big(0); let sumOfTimeWeightedInvestments = new Big(0);
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0);
({
totalDividend,
totalDividendInBaseCurrency,
totalInterest,
totalInterestInBaseCurrency,
totalValuables,
totalValuablesInBaseCurrency,
totalLiabilities,
totalLiabilitiesInBaseCurrency,
totalUnits,
investmentAtStartDate,
totalInvestment,
investmentAtStartDateWithCurrencyEffect,
totalInvestmentWithCurrencyEffect,
valueAtStartDate,
valueAtStartDateWithCurrencyEffect,
totalQuantityFromBuyTransactions,
totalInvestmentFromBuyTransactions,
totalInvestmentFromBuyTransactionsWithCurrencyEffect,
initialValue,
initialValueWithCurrencyEffect,
fees,
feesWithCurrencyEffect,
lastAveragePrice,
lastAveragePriceWithCurrencyEffect,
grossPerformanceFromSells,
grossPerformanceFromSellsWithCurrencyEffect,
grossPerformance,
grossPerformanceWithCurrencyEffect,
feesAtStartDate,
feesAtStartDateWithCurrencyEffect,
grossPerformanceAtStartDate,
grossPerformanceAtStartDateWithCurrencyEffect,
totalInvestmentDays,
sumOfTimeWeightedInvestments,
sumOfTimeWeightedInvestmentsWithCurrencyEffect
} = this.handleOrders(
orders,
exchangeRates,
totalDividend,
totalDividendInBaseCurrency,
totalInterest,
totalInterestInBaseCurrency,
totalValuables,
totalValuablesInBaseCurrency,
totalLiabilities,
totalLiabilitiesInBaseCurrency,
indexOfStartOrder,
unitPriceAtStartDate,
currentExchangeRate,
marketSymbolMap,
symbol,
totalUnits,
investmentAtStartDate,
totalInvestment,
investmentAtStartDateWithCurrencyEffect,
totalInvestmentWithCurrencyEffect,
valueAtStartDate,
valueAtStartDateWithCurrencyEffect,
totalQuantityFromBuyTransactions,
totalInvestmentFromBuyTransactions,
totalInvestmentFromBuyTransactionsWithCurrencyEffect,
initialValue,
initialValueWithCurrencyEffect,
fees,
feesWithCurrencyEffect,
lastAveragePrice,
lastAveragePriceWithCurrencyEffect,
grossPerformanceFromSells,
grossPerformanceFromSellsWithCurrencyEffect,
grossPerformance,
grossPerformanceWithCurrencyEffect,
feesAtStartDate,
feesAtStartDateWithCurrencyEffect,
grossPerformanceAtStartDate,
grossPerformanceAtStartDateWithCurrencyEffect,
totalInvestmentDays,
sumOfTimeWeightedInvestments,
sumOfTimeWeightedInvestmentsWithCurrencyEffect,
isChartMode,
currentValues,
currentValuesWithCurrencyEffect,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect,
indexOfEndOrder
));
const totalGrossPerformance = grossPerformance.minus(
grossPerformanceAtStartDate
);
const totalGrossPerformanceWithCurrencyEffect =
grossPerformanceWithCurrencyEffect.minus(
grossPerformanceAtStartDateWithCurrencyEffect
);
const totalNetPerformance = grossPerformance
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
const totalNetPerformanceWithCurrencyEffect =
grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.minus(feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect));
const timeWeightedAverageInvestmentBetweenStartAndEndDate =
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: new Big(0);
const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect =
totalInvestmentDays > 0
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
totalInvestmentDays
)
: new Big(0);
const grossPerformancePercentage =
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
? totalGrossPerformance.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate
)
: new Big(0);
const grossPerformancePercentageWithCurrencyEffect =
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt(
0
)
? totalGrossPerformanceWithCurrencyEffect.div(
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
)
: new Big(0);
const feesPerUnit = totalUnits.gt(0)
? fees.minus(feesAtStartDate).div(totalUnits)
: new Big(0);
const feesPerUnitWithCurrencyEffect = totalUnits.gt(0)
? feesWithCurrencyEffect
.minus(feesAtStartDateWithCurrencyEffect)
.div(totalUnits)
: new Big(0);
const netPerformancePercentage =
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
? totalNetPerformance.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate
)
: new Big(0);
const netPerformancePercentageWithCurrencyEffect =
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt(
0
)
? totalNetPerformanceWithCurrencyEffect.div(
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
)
: new Big(0);
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
`
${symbol}
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
2
)} -> ${unitPriceAtEndDate.toFixed(2)}
Total investment: ${totalInvestment.toFixed(2)}
Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed(
2
)}
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed(
2
)}
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
2
)}
Total dividend: ${totalDividend.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed(
2
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed(
2
)} / ${grossPerformancePercentageWithCurrencyEffect
.mul(100)
.toFixed(2)}%
Fees per unit: ${feesPerUnit.toFixed(2)}
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed(
2
)}
Net performance: ${totalNetPerformance.toFixed(
2
)} / ${netPerformancePercentage.mul(100).toFixed(2)}%
Net performance with currency effect: ${totalNetPerformanceWithCurrencyEffect.toFixed(
2
)} / ${netPerformancePercentageWithCurrencyEffect.mul(100).toFixed(2)}%`
);
}
let unitPrices = Object.keys(marketSymbolMap)
.map((date) => {
return { [date]: marketSymbolMap[date][symbol] };
})
.reduce((map, u) => {
return { ...u, ...map };
}, {});
return {
currentValues,
currentValuesWithCurrencyEffect,
unitPrices,
feesWithCurrencyEffect,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
initialValue,
initialValueWithCurrencyEffect,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
netPerformanceValuesPercentage: {},
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect,
totalAccountBalanceInBaseCurrency,
totalDividend,
totalDividendInBaseCurrency,
totalInterest,
totalInterestInBaseCurrency,
totalInvestment,
totalInvestmentWithCurrencyEffect,
totalLiabilities,
totalLiabilitiesInBaseCurrency,
totalValuables,
totalValuablesInBaseCurrency,
grossPerformance: totalGrossPerformance,
grossPerformanceWithCurrencyEffect:
totalGrossPerformanceWithCurrencyEffect,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance,
netPerformanceWithCurrencyEffect: totalNetPerformanceWithCurrencyEffect,
timeWeightedInvestment:
timeWeightedAverageInvestmentBetweenStartAndEndDate,
timeWeightedInvestmentWithCurrencyEffect:
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
};
}
@LogPerformance
protected handleOrders(
orders: PortfolioOrderItem[],
exchangeRates: { [dateString: string]: number },
totalDividend,
totalDividendInBaseCurrency,
totalInterest,
totalInterestInBaseCurrency,
totalValuables,
totalValuablesInBaseCurrency,
totalLiabilities,
totalLiabilitiesInBaseCurrency,
indexOfStartOrder: number,
unitPriceAtStartDate: Big,
currentExchangeRate: number,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
symbol: string,
totalUnits,
investmentAtStartDate: Big,
totalInvestment,
investmentAtStartDateWithCurrencyEffect: Big,
totalInvestmentWithCurrencyEffect,
valueAtStartDate: Big,
valueAtStartDateWithCurrencyEffect: Big,
totalQuantityFromBuyTransactions,
totalInvestmentFromBuyTransactions,
totalInvestmentFromBuyTransactionsWithCurrencyEffect,
initialValue: Big,
initialValueWithCurrencyEffect: Big,
fees,
feesWithCurrencyEffect,
lastAveragePrice,
lastAveragePriceWithCurrencyEffect,
grossPerformanceFromSells,
grossPerformanceFromSellsWithCurrencyEffect,
grossPerformance,
grossPerformanceWithCurrencyEffect,
feesAtStartDate,
feesAtStartDateWithCurrencyEffect,
grossPerformanceAtStartDate,
grossPerformanceAtStartDateWithCurrencyEffect,
totalInvestmentDays: number,
sumOfTimeWeightedInvestments,
sumOfTimeWeightedInvestmentsWithCurrencyEffect,
isChartMode: boolean,
currentValues: { [date: string]: Big },
currentValuesWithCurrencyEffect: { [date: string]: Big },
netPerformanceValues: { [date: string]: Big },
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big },
investmentValuesAccumulated: { [date: string]: Big },
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big },
investmentValuesWithCurrencyEffect: { [date: string]: Big },
timeWeightedInvestmentValues: { [date: string]: Big },
timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big },
indexOfEndOrder: number
) {
for (let i = 0; i < orders.length; i += 1) { for (let i = 0; i < orders.length; i += 1) {
const order = orders[i]; const order = orders[i];
@ -707,27 +392,35 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
const exchangeRateAtOrderDate = exchangeRates[order.date]; const exchangeRateAtOrderDate = exchangeRates[order.date];
({ if (order.type === 'DIVIDEND') {
totalDividend, const dividend = order.quantity.mul(order.unitPrice);
totalDividendInBaseCurrency,
totalInterest, totalDividend = totalDividend.plus(dividend);
totalInterestInBaseCurrency, totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
totalValuables, dividend.mul(exchangeRateAtOrderDate ?? 1)
totalValuablesInBaseCurrency, );
totalLiabilities, } else if (order.type === 'INTEREST') {
totalLiabilitiesInBaseCurrency const interest = order.quantity.mul(order.unitPrice);
} = this.handleOrderType(
order, totalInterest = totalInterest.plus(interest);
totalDividend, totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
totalDividendInBaseCurrency, interest.mul(exchangeRateAtOrderDate ?? 1)
exchangeRateAtOrderDate, );
totalInterest, } else if (order.type === 'ITEM') {
totalInterestInBaseCurrency, const valuables = order.quantity.mul(order.unitPrice);
totalValuables,
totalValuablesInBaseCurrency, totalValuables = totalValuables.plus(valuables);
totalLiabilities, totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus(
totalLiabilitiesInBaseCurrency valuables.mul(exchangeRateAtOrderDate ?? 1)
)); );
} else if (order.type === 'LIABILITY') {
const liabilities = order.quantity.mul(order.unitPrice);
totalLiabilities = totalLiabilities.plus(liabilities);
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
liabilities.mul(exchangeRateAtOrderDate ?? 1)
);
}
if (order.itemType === 'start') { if (order.itemType === 'start') {
// Take the unit price of the order as the market price if there are no // Take the unit price of the order as the market price if there are no
@ -745,15 +438,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
); );
} }
if (order.type === 'STAKE') { const unitPrice = ['BUY', 'SELL'].includes(order.type)
order.unitPrice = marketSymbolMap[order.date]?.[symbol] ?? new Big(0);
}
if (order.type === 'STAKE') {
order.unitPrice = marketSymbolMap[order.date]?.[symbol] ?? new Big(0);
}
const unitPrice = ['BUY', 'SELL', 'STAKE'].includes(order.type)
? order.unitPrice ? order.unitPrice
: order.unitPriceFromMarketData; : order.unitPriceFromMarketData;
@ -787,23 +472,38 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
let transactionInvestment = new Big(0); let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0); let transactionInvestmentWithCurrencyEffect = new Big(0);
({ if (order.type === 'BUY') {
transactionInvestment, transactionInvestment = order.quantity
transactionInvestmentWithCurrencyEffect, .mul(order.unitPriceInBaseCurrency)
totalQuantityFromBuyTransactions, .mul(getFactor(order.type));
totalInvestmentFromBuyTransactions,
totalInvestmentFromBuyTransactionsWithCurrencyEffect transactionInvestmentWithCurrencyEffect = order.quantity
} = this.handleBuyAndSellOrders( .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
order, .mul(getFactor(order.type));
transactionInvestment,
transactionInvestmentWithCurrencyEffect, totalQuantityFromBuyTransactions =
totalQuantityFromBuyTransactions, totalQuantityFromBuyTransactions.plus(order.quantity);
totalInvestmentFromBuyTransactions,
totalInvestmentFromBuyTransactionsWithCurrencyEffect, totalInvestmentFromBuyTransactions =
totalUnits, totalInvestmentFromBuyTransactions.plus(transactionInvestment);
totalInvestment,
totalInvestmentFromBuyTransactionsWithCurrencyEffect =
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
} else if (order.type === 'SELL') {
if (totalUnits.gt(0)) {
transactionInvestment = totalInvestment
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect totalInvestmentWithCurrencyEffect
)); .div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type));
}
}
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('order.quantity', order.quantity.toNumber()); console.log('order.quantity', order.quantity.toNumber());
@ -815,140 +515,90 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
); );
} }
let valueOfInvestment; const totalInvestmentBeforeTransaction = totalInvestment;
let valueOfInvestmentWithCurrencyEffect;
let totalInvestmentBeforeTransaction;
let totalInvestmentBeforeTransactionWithCurrencyEffect;
({
valueOfInvestment,
valueOfInvestmentWithCurrencyEffect,
totalInvestmentBeforeTransaction,
totalInvestmentBeforeTransactionWithCurrencyEffect,
totalInvestment,
totalInvestmentWithCurrencyEffect,
initialValue,
initialValueWithCurrencyEffect,
fees,
feesWithCurrencyEffect,
totalUnits
} = this.calculateInvestmentValues(
totalInvestment,
totalInvestmentWithCurrencyEffect,
transactionInvestment,
transactionInvestmentWithCurrencyEffect,
i,
indexOfStartOrder,
initialValue,
valueOfInvestmentBeforeTransaction,
initialValueWithCurrencyEffect,
valueOfInvestmentBeforeTransactionWithCurrencyEffect,
fees,
order,
feesWithCurrencyEffect,
totalUnits
));
({
lastAveragePrice,
lastAveragePriceWithCurrencyEffect,
grossPerformanceFromSells,
grossPerformanceFromSellsWithCurrencyEffect,
grossPerformance,
grossPerformanceWithCurrencyEffect
} = this.calculatePerformances(
order,
lastAveragePrice,
lastAveragePriceWithCurrencyEffect,
grossPerformanceFromSells,
grossPerformanceFromSellsWithCurrencyEffect,
totalQuantityFromBuyTransactions,
totalInvestmentFromBuyTransactions,
totalInvestmentFromBuyTransactionsWithCurrencyEffect,
valueOfInvestment,
totalInvestment,
valueOfInvestmentWithCurrencyEffect,
totalInvestmentWithCurrencyEffect,
grossPerformance,
grossPerformanceWithCurrencyEffect
));
if (order.itemType === 'start') { const totalInvestmentBeforeTransactionWithCurrencyEffect =
feesAtStartDate = fees; totalInvestmentWithCurrencyEffect;
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
grossPerformanceAtStartDate = grossPerformance;
grossPerformanceAtStartDateWithCurrencyEffect = totalInvestment = totalInvestment.plus(transactionInvestment);
grossPerformanceWithCurrencyEffect;
}
totalInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
if (i >= indexOfStartOrder && !initialValue) {
if ( if (
i > indexOfStartOrder && i === indexOfStartOrder &&
['BUY', 'SELL', 'STAKE'].includes(order.type) !valueOfInvestmentBeforeTransaction.eq(0)
) { ) {
// Only consider periods with an investment for the calculation of initialValue = valueOfInvestmentBeforeTransaction;
// the time weighted investment
({ initialValueWithCurrencyEffect =
totalInvestmentDays, valueOfInvestmentBeforeTransactionWithCurrencyEffect;
sumOfTimeWeightedInvestments, } else if (transactionInvestment.gt(0)) {
sumOfTimeWeightedInvestmentsWithCurrencyEffect initialValue = transactionInvestment;
} = this.calculateTimeWeightedInvestments(
valueOfInvestmentBeforeTransaction, initialValueWithCurrencyEffect =
order, transactionInvestmentWithCurrencyEffect;
orders, }
i,
totalInvestmentDays,
sumOfTimeWeightedInvestments,
valueAtStartDate,
investmentAtStartDate,
totalInvestmentBeforeTransaction,
sumOfTimeWeightedInvestmentsWithCurrencyEffect,
valueAtStartDateWithCurrencyEffect,
investmentAtStartDateWithCurrencyEffect,
totalInvestmentBeforeTransactionWithCurrencyEffect,
isChartMode,
currentValues,
valueOfInvestment,
currentValuesWithCurrencyEffect,
valueOfInvestmentWithCurrencyEffect,
netPerformanceValues,
grossPerformance,
grossPerformanceAtStartDate,
fees,
feesAtStartDate,
netPerformanceValuesWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
grossPerformanceAtStartDateWithCurrencyEffect,
feesWithCurrencyEffect,
feesAtStartDateWithCurrencyEffect,
investmentValuesAccumulated,
totalInvestment,
investmentValuesAccumulatedWithCurrencyEffect,
totalInvestmentWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
transactionInvestmentWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect
));
} }
if (PortfolioCalculator.ENABLE_LOGGING) { fees = fees.plus(order.feeInBaseCurrency ?? 0);
console.log('totalInvestment', totalInvestment.toNumber());
console.log( feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
'totalInvestmentWithCurrencyEffect', order.feeInBaseCurrencyWithCurrencyEffect ?? 0
totalInvestmentWithCurrencyEffect.toNumber()
); );
console.log( totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
'totalGrossPerformance',
grossPerformance.minus(grossPerformanceAtStartDate).toNumber() const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
order.unitPriceInBaseCurrencyWithCurrencyEffect
);
const grossPerformanceFromSell =
order.type === 'SELL'
? order.unitPriceInBaseCurrency
.minus(lastAveragePrice)
.mul(order.quantity)
: new Big(0);
const grossPerformanceFromSellWithCurrencyEffect =
order.type === 'SELL'
? order.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePriceWithCurrencyEffect)
.mul(order.quantity)
: new Big(0);
grossPerformanceFromSells = grossPerformanceFromSells.plus(
grossPerformanceFromSell
);
grossPerformanceFromSellsWithCurrencyEffect =
grossPerformanceFromSellsWithCurrencyEffect.plus(
grossPerformanceFromSellWithCurrencyEffect
);
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
? new Big(0)
: totalInvestmentFromBuyTransactions.div(
totalQuantityFromBuyTransactions
);
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
0
)
? new Big(0)
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
totalQuantityFromBuyTransactions
); );
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log( console.log(
'totalGrossPerformanceWithCurrencyEffect', 'grossPerformanceFromSells',
grossPerformanceWithCurrencyEffect grossPerformanceFromSells.toNumber()
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.toNumber()
); );
console.log( console.log(
'grossPerformanceFromSellWithCurrencyEffect', 'grossPerformanceFromSellWithCurrencyEffect',
@ -1226,9 +876,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
// differs from the buying price. // differs from the buying price.
dateRange === 'max' dateRange === 'max'
? new Big(0) ? new Big(0)
: netPerformanceValuesWithCurrencyEffect[ : (netPerformanceValuesWithCurrencyEffect[
format(startDate, DATE_FORMAT) format(startDate, DATE_FORMAT)
] ?? new Big(0) ] ?? new Big(0))
) ?? new Big(0); ) ?? new Big(0);
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0)
@ -1274,6 +924,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
].toFixed(2)}%` ].toFixed(2)}%`
); );
} }
return { return {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
@ -1292,10 +943,15 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
netPerformanceWithCurrencyEffectMap, netPerformanceWithCurrencyEffectMap,
timeWeightedInvestmentValues, timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect, timeWeightedInvestmentValuesWithCurrencyEffect,
totalAccountBalanceInBaseCurrency,
totalDividend, totalDividend,
totalDividendInBaseCurrency, totalDividendInBaseCurrency,
totalInterest, totalInterest,
totalInterestInBaseCurrency, totalInterestInBaseCurrency,
totalInvestment,
totalInvestmentWithCurrencyEffect,
totalLiabilities,
totalLiabilitiesInBaseCurrency,
totalValuables, totalValuables,
totalValuablesInBaseCurrency, totalValuablesInBaseCurrency,
grossPerformance: totalGrossPerformance, grossPerformance: totalGrossPerformance,
@ -1306,7 +962,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
timeWeightedInvestment: timeWeightedInvestment:
timeWeightedAverageInvestmentBetweenStartAndEndDate, timeWeightedAverageInvestmentBetweenStartAndEndDate,
timeWeightedInvestmentWithCurrencyEffect: timeWeightedInvestmentWithCurrencyEffect:
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect,
netPerformanceValuesPercentage: {},
unitPrices: {}
}; };
} }
} }

Loading…
Cancel
Save