mirror of https://github.com/ghostfolio/ghostfolio
Francisco Silva
10 months ago
committed by
GitHub
20 changed files with 1361 additions and 1052 deletions
@ -0,0 +1,37 @@ |
|||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
|||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; |
|||
import { |
|||
SymbolMetrics, |
|||
TimelinePosition, |
|||
UniqueAsset |
|||
} from '@ghostfolio/common/interfaces'; |
|||
|
|||
export class MWRPortfolioCalculator extends PortfolioCalculator { |
|||
protected calculateOverallPerformance( |
|||
positions: TimelinePosition[] |
|||
): CurrentPositions { |
|||
throw new Error('Method not implemented.'); |
|||
} |
|||
|
|||
protected getSymbolMetrics({ |
|||
dataSource, |
|||
end, |
|||
exchangeRates, |
|||
isChartMode = false, |
|||
marketSymbolMap, |
|||
start, |
|||
step = 1, |
|||
symbol |
|||
}: { |
|||
end: Date; |
|||
exchangeRates: { [dateString: string]: number }; |
|||
isChartMode?: boolean; |
|||
marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
}; |
|||
start: Date; |
|||
step?: number; |
|||
} & UniqueAsset): SymbolMetrics { |
|||
throw new Error('Method not implemented.'); |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
export const activityDummyData = { |
|||
accountId: undefined, |
|||
accountUserId: undefined, |
|||
comment: undefined, |
|||
createdAt: new Date(), |
|||
feeInBaseCurrency: undefined, |
|||
id: undefined, |
|||
isDraft: false, |
|||
symbolProfileId: undefined, |
|||
updatedAt: new Date(), |
|||
userId: undefined, |
|||
value: undefined, |
|||
valueInBaseCurrency: undefined |
|||
}; |
|||
|
|||
export const symbolProfileDummyData = { |
|||
activitiesCount: undefined, |
|||
assetClass: undefined, |
|||
assetSubClass: undefined, |
|||
countries: [], |
|||
createdAt: undefined, |
|||
id: undefined, |
|||
sectors: [], |
|||
updatedAt: undefined |
|||
}; |
@ -0,0 +1,51 @@ |
|||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
|
|||
import { Injectable } from '@nestjs/common'; |
|||
|
|||
import { MWRPortfolioCalculator } from './mwr/portfolio-calculator'; |
|||
import { PortfolioCalculator } from './portfolio-calculator'; |
|||
import { TWRPortfolioCalculator } from './twr/portfolio-calculator'; |
|||
|
|||
export enum PerformanceCalculationType { |
|||
MWR = 'MWR', // Money-Weighted Rate of Return
|
|||
TWR = 'TWR' // Time-Weighted Rate of Return
|
|||
} |
|||
|
|||
@Injectable() |
|||
export class PortfolioCalculatorFactory { |
|||
public constructor( |
|||
private readonly currentRateService: CurrentRateService, |
|||
private readonly exchangeRateDataService: ExchangeRateDataService |
|||
) {} |
|||
|
|||
public createCalculator({ |
|||
activities, |
|||
calculationType, |
|||
currency |
|||
}: { |
|||
activities: Activity[]; |
|||
calculationType: PerformanceCalculationType; |
|||
currency: string; |
|||
}): PortfolioCalculator { |
|||
switch (calculationType) { |
|||
case PerformanceCalculationType.MWR: |
|||
return new MWRPortfolioCalculator({ |
|||
activities, |
|||
currency, |
|||
currentRateService: this.currentRateService, |
|||
exchangeRateDataService: this.exchangeRateDataService |
|||
}); |
|||
case PerformanceCalculationType.TWR: |
|||
return new TWRPortfolioCalculator({ |
|||
activities, |
|||
currency, |
|||
currentRateService: this.currentRateService, |
|||
exchangeRateDataService: this.exchangeRateDataService |
|||
}); |
|||
default: |
|||
throw new Error('Invalid calculation type'); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,788 @@ |
|||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; |
|||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; |
|||
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; |
|||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; |
|||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
|||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
|||
import { |
|||
DataProviderInfo, |
|||
HistoricalDataItem, |
|||
InvestmentItem, |
|||
ResponseError, |
|||
SymbolMetrics, |
|||
TimelinePosition, |
|||
UniqueAsset |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { GroupBy } from '@ghostfolio/common/types'; |
|||
|
|||
import { Big } from 'big.js'; |
|||
import { |
|||
eachDayOfInterval, |
|||
endOfDay, |
|||
format, |
|||
isBefore, |
|||
isSameDay, |
|||
max, |
|||
subDays |
|||
} from 'date-fns'; |
|||
import { isNumber, last, uniq } from 'lodash'; |
|||
|
|||
export abstract class PortfolioCalculator { |
|||
protected static readonly ENABLE_LOGGING = false; |
|||
|
|||
protected orders: PortfolioOrder[]; |
|||
|
|||
private currency: string; |
|||
private currentRateService: CurrentRateService; |
|||
private dataProviderInfos: DataProviderInfo[]; |
|||
private exchangeRateDataService: ExchangeRateDataService; |
|||
private transactionPoints: TransactionPoint[]; |
|||
|
|||
public constructor({ |
|||
activities, |
|||
currency, |
|||
currentRateService, |
|||
exchangeRateDataService |
|||
}: { |
|||
activities: Activity[]; |
|||
currency: string; |
|||
currentRateService: CurrentRateService; |
|||
exchangeRateDataService: ExchangeRateDataService; |
|||
}) { |
|||
this.currency = currency; |
|||
this.currentRateService = currentRateService; |
|||
this.exchangeRateDataService = exchangeRateDataService; |
|||
this.orders = activities.map( |
|||
({ date, fee, quantity, SymbolProfile, type, unitPrice }) => { |
|||
return { |
|||
SymbolProfile, |
|||
type, |
|||
date: format(date, DATE_FORMAT), |
|||
fee: new Big(fee), |
|||
quantity: new Big(quantity), |
|||
unitPrice: new Big(unitPrice) |
|||
}; |
|||
} |
|||
); |
|||
|
|||
this.orders.sort((a, b) => { |
|||
return a.date?.localeCompare(b.date); |
|||
}); |
|||
|
|||
this.computeTransactionPoints(); |
|||
} |
|||
|
|||
protected abstract calculateOverallPerformance( |
|||
positions: TimelinePosition[] |
|||
): CurrentPositions; |
|||
|
|||
public getAnnualizedPerformancePercent({ |
|||
daysInMarket, |
|||
netPerformancePercent |
|||
}: { |
|||
daysInMarket: number; |
|||
netPerformancePercent: Big; |
|||
}): Big { |
|||
if (isNumber(daysInMarket) && daysInMarket > 0) { |
|||
const exponent = new Big(365).div(daysInMarket).toNumber(); |
|||
return new Big( |
|||
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) |
|||
).minus(1); |
|||
} |
|||
|
|||
return new Big(0); |
|||
} |
|||
|
|||
public async getChartData({ |
|||
end = new Date(Date.now()), |
|||
start, |
|||
step = 1 |
|||
}: { |
|||
end?: Date; |
|||
start: Date; |
|||
step?: number; |
|||
}): Promise<HistoricalDataItem[]> { |
|||
const symbols: { [symbol: string]: boolean } = {}; |
|||
|
|||
const transactionPointsBeforeEndDate = |
|||
this.transactionPoints?.filter((transactionPoint) => { |
|||
return isBefore(parseDate(transactionPoint.date), end); |
|||
}) ?? []; |
|||
|
|||
const currencies: { [symbol: string]: string } = {}; |
|||
const dataGatheringItems: IDataGatheringItem[] = []; |
|||
const firstIndex = transactionPointsBeforeEndDate.length; |
|||
|
|||
let dates = eachDayOfInterval({ start, end }, { step }).map((date) => { |
|||
return resetHours(date); |
|||
}); |
|||
|
|||
const includesEndDate = isSameDay(last(dates), end); |
|||
|
|||
if (!includesEndDate) { |
|||
dates.push(resetHours(end)); |
|||
} |
|||
|
|||
if (transactionPointsBeforeEndDate.length > 0) { |
|||
for (const { |
|||
currency, |
|||
dataSource, |
|||
symbol |
|||
} of transactionPointsBeforeEndDate[firstIndex - 1].items) { |
|||
dataGatheringItems.push({ |
|||
dataSource, |
|||
symbol |
|||
}); |
|||
currencies[symbol] = currency; |
|||
symbols[symbol] = true; |
|||
} |
|||
} |
|||
|
|||
const { dataProviderInfos, values: marketSymbols } = |
|||
await this.currentRateService.getValues({ |
|||
dataGatheringItems, |
|||
dateQuery: { |
|||
in: dates |
|||
} |
|||
}); |
|||
|
|||
this.dataProviderInfos = dataProviderInfos; |
|||
|
|||
const marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
} = {}; |
|||
|
|||
let exchangeRatesByCurrency = |
|||
await this.exchangeRateDataService.getExchangeRatesByCurrency({ |
|||
currencies: uniq(Object.values(currencies)), |
|||
endDate: endOfDay(end), |
|||
startDate: this.getStartDate(), |
|||
targetCurrency: this.currency |
|||
}); |
|||
|
|||
for (const marketSymbol of marketSymbols) { |
|||
const dateString = format(marketSymbol.date, DATE_FORMAT); |
|||
if (!marketSymbolMap[dateString]) { |
|||
marketSymbolMap[dateString] = {}; |
|||
} |
|||
if (marketSymbol.marketPrice) { |
|||
marketSymbolMap[dateString][marketSymbol.symbol] = new Big( |
|||
marketSymbol.marketPrice |
|||
); |
|||
} |
|||
} |
|||
|
|||
const accumulatedValuesByDate: { |
|||
[date: string]: { |
|||
investmentValueWithCurrencyEffect: Big; |
|||
totalCurrentValue: Big; |
|||
totalCurrentValueWithCurrencyEffect: Big; |
|||
totalInvestmentValue: Big; |
|||
totalInvestmentValueWithCurrencyEffect: Big; |
|||
totalNetPerformanceValue: Big; |
|||
totalNetPerformanceValueWithCurrencyEffect: Big; |
|||
totalTimeWeightedInvestmentValue: Big; |
|||
totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; |
|||
}; |
|||
} = {}; |
|||
|
|||
const valuesBySymbol: { |
|||
[symbol: string]: { |
|||
currentValues: { [date: string]: Big }; |
|||
currentValuesWithCurrencyEffect: { [date: string]: Big }; |
|||
investmentValuesAccumulated: { [date: string]: Big }; |
|||
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big }; |
|||
investmentValuesWithCurrencyEffect: { [date: string]: Big }; |
|||
netPerformanceValues: { [date: string]: Big }; |
|||
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; |
|||
timeWeightedInvestmentValues: { [date: string]: Big }; |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; |
|||
}; |
|||
} = {}; |
|||
|
|||
for (const symbol of Object.keys(symbols)) { |
|||
const { |
|||
currentValues, |
|||
currentValuesWithCurrencyEffect, |
|||
investmentValuesAccumulated, |
|||
investmentValuesAccumulatedWithCurrencyEffect, |
|||
investmentValuesWithCurrencyEffect, |
|||
netPerformanceValues, |
|||
netPerformanceValuesWithCurrencyEffect, |
|||
timeWeightedInvestmentValues, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect |
|||
} = this.getSymbolMetrics({ |
|||
end, |
|||
marketSymbolMap, |
|||
start, |
|||
step, |
|||
symbol, |
|||
dataSource: null, |
|||
exchangeRates: |
|||
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`], |
|||
isChartMode: true |
|||
}); |
|||
|
|||
valuesBySymbol[symbol] = { |
|||
currentValues, |
|||
currentValuesWithCurrencyEffect, |
|||
investmentValuesAccumulated, |
|||
investmentValuesAccumulatedWithCurrencyEffect, |
|||
investmentValuesWithCurrencyEffect, |
|||
netPerformanceValues, |
|||
netPerformanceValuesWithCurrencyEffect, |
|||
timeWeightedInvestmentValues, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect |
|||
}; |
|||
} |
|||
|
|||
for (const currentDate of dates) { |
|||
const dateString = format(currentDate, DATE_FORMAT); |
|||
|
|||
for (const symbol of Object.keys(valuesBySymbol)) { |
|||
const symbolValues = valuesBySymbol[symbol]; |
|||
|
|||
const currentValue = |
|||
symbolValues.currentValues?.[dateString] ?? new Big(0); |
|||
|
|||
const currentValueWithCurrencyEffect = |
|||
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? |
|||
new Big(0); |
|||
|
|||
const investmentValueAccumulated = |
|||
symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0); |
|||
|
|||
const investmentValueAccumulatedWithCurrencyEffect = |
|||
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[ |
|||
dateString |
|||
] ?? new Big(0); |
|||
|
|||
const investmentValueWithCurrencyEffect = |
|||
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? |
|||
new Big(0); |
|||
|
|||
const netPerformanceValue = |
|||
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); |
|||
|
|||
const netPerformanceValueWithCurrencyEffect = |
|||
symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ?? |
|||
new Big(0); |
|||
|
|||
const timeWeightedInvestmentValue = |
|||
symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0); |
|||
|
|||
const timeWeightedInvestmentValueWithCurrencyEffect = |
|||
symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[ |
|||
dateString |
|||
] ?? new Big(0); |
|||
|
|||
accumulatedValuesByDate[dateString] = { |
|||
investmentValueWithCurrencyEffect: ( |
|||
accumulatedValuesByDate[dateString] |
|||
?.investmentValueWithCurrencyEffect ?? new Big(0) |
|||
).add(investmentValueWithCurrencyEffect), |
|||
totalCurrentValue: ( |
|||
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) |
|||
).add(currentValue), |
|||
totalCurrentValueWithCurrencyEffect: ( |
|||
accumulatedValuesByDate[dateString] |
|||
?.totalCurrentValueWithCurrencyEffect ?? new Big(0) |
|||
).add(currentValueWithCurrencyEffect), |
|||
totalInvestmentValue: ( |
|||
accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? |
|||
new Big(0) |
|||
).add(investmentValueAccumulated), |
|||
totalInvestmentValueWithCurrencyEffect: ( |
|||
accumulatedValuesByDate[dateString] |
|||
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) |
|||
).add(investmentValueAccumulatedWithCurrencyEffect), |
|||
totalNetPerformanceValue: ( |
|||
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? |
|||
new Big(0) |
|||
).add(netPerformanceValue), |
|||
totalNetPerformanceValueWithCurrencyEffect: ( |
|||
accumulatedValuesByDate[dateString] |
|||
?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0) |
|||
).add(netPerformanceValueWithCurrencyEffect), |
|||
totalTimeWeightedInvestmentValue: ( |
|||
accumulatedValuesByDate[dateString] |
|||
?.totalTimeWeightedInvestmentValue ?? new Big(0) |
|||
).add(timeWeightedInvestmentValue), |
|||
totalTimeWeightedInvestmentValueWithCurrencyEffect: ( |
|||
accumulatedValuesByDate[dateString] |
|||
?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0) |
|||
).add(timeWeightedInvestmentValueWithCurrencyEffect) |
|||
}; |
|||
} |
|||
} |
|||
|
|||
return Object.entries(accumulatedValuesByDate).map(([date, values]) => { |
|||
const { |
|||
investmentValueWithCurrencyEffect, |
|||
totalCurrentValue, |
|||
totalCurrentValueWithCurrencyEffect, |
|||
totalInvestmentValue, |
|||
totalInvestmentValueWithCurrencyEffect, |
|||
totalNetPerformanceValue, |
|||
totalNetPerformanceValueWithCurrencyEffect, |
|||
totalTimeWeightedInvestmentValue, |
|||
totalTimeWeightedInvestmentValueWithCurrencyEffect |
|||
} = values; |
|||
|
|||
const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0) |
|||
? 0 |
|||
: totalNetPerformanceValue |
|||
.div(totalTimeWeightedInvestmentValue) |
|||
.mul(100) |
|||
.toNumber(); |
|||
|
|||
const netPerformanceInPercentageWithCurrencyEffect = |
|||
totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0) |
|||
? 0 |
|||
: totalNetPerformanceValueWithCurrencyEffect |
|||
.div(totalTimeWeightedInvestmentValueWithCurrencyEffect) |
|||
.mul(100) |
|||
.toNumber(); |
|||
|
|||
return { |
|||
date, |
|||
netPerformanceInPercentage, |
|||
netPerformanceInPercentageWithCurrencyEffect, |
|||
investmentValueWithCurrencyEffect: |
|||
investmentValueWithCurrencyEffect.toNumber(), |
|||
netPerformance: totalNetPerformanceValue.toNumber(), |
|||
netPerformanceWithCurrencyEffect: |
|||
totalNetPerformanceValueWithCurrencyEffect.toNumber(), |
|||
totalInvestment: totalInvestmentValue.toNumber(), |
|||
totalInvestmentValueWithCurrencyEffect: |
|||
totalInvestmentValueWithCurrencyEffect.toNumber(), |
|||
value: totalCurrentValue.toNumber(), |
|||
valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber() |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public async getCurrentPositions( |
|||
start: Date, |
|||
end?: Date |
|||
): Promise<CurrentPositions> { |
|||
const lastTransactionPoint = last(this.transactionPoints); |
|||
|
|||
let endDate = end; |
|||
|
|||
if (!endDate) { |
|||
endDate = new Date(Date.now()); |
|||
|
|||
if (lastTransactionPoint) { |
|||
endDate = max([endDate, parseDate(lastTransactionPoint.date)]); |
|||
} |
|||
} |
|||
|
|||
const transactionPoints = this.transactionPoints?.filter(({ date }) => { |
|||
return isBefore(parseDate(date), endDate); |
|||
}); |
|||
|
|||
if (!transactionPoints.length) { |
|||
return { |
|||
currentValueInBaseCurrency: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: false, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
netPerformanceWithCurrencyEffect: new Big(0), |
|||
positions: [], |
|||
totalInvestment: new Big(0), |
|||
totalInvestmentWithCurrencyEffect: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
const currencies: { [symbol: string]: string } = {}; |
|||
const dataGatheringItems: IDataGatheringItem[] = []; |
|||
let dates: Date[] = []; |
|||
let firstIndex = transactionPoints.length; |
|||
let firstTransactionPoint: TransactionPoint = null; |
|||
|
|||
dates.push(resetHours(start)); |
|||
|
|||
for (const { currency, dataSource, symbol } of transactionPoints[ |
|||
firstIndex - 1 |
|||
].items) { |
|||
dataGatheringItems.push({ |
|||
dataSource, |
|||
symbol |
|||
}); |
|||
|
|||
currencies[symbol] = currency; |
|||
} |
|||
|
|||
for (let i = 0; i < transactionPoints.length; i++) { |
|||
if ( |
|||
!isBefore(parseDate(transactionPoints[i].date), start) && |
|||
firstTransactionPoint === null |
|||
) { |
|||
firstTransactionPoint = transactionPoints[i]; |
|||
firstIndex = i; |
|||
} |
|||
|
|||
if (firstTransactionPoint !== null) { |
|||
dates.push(resetHours(parseDate(transactionPoints[i].date))); |
|||
} |
|||
} |
|||
|
|||
dates.push(resetHours(endDate)); |
|||
|
|||
// Add dates of last week for fallback
|
|||
dates.push(subDays(resetHours(new Date()), 7)); |
|||
dates.push(subDays(resetHours(new Date()), 6)); |
|||
dates.push(subDays(resetHours(new Date()), 5)); |
|||
dates.push(subDays(resetHours(new Date()), 4)); |
|||
dates.push(subDays(resetHours(new Date()), 3)); |
|||
dates.push(subDays(resetHours(new Date()), 2)); |
|||
dates.push(subDays(resetHours(new Date()), 1)); |
|||
dates.push(resetHours(new Date())); |
|||
|
|||
dates = uniq( |
|||
dates.map((date) => { |
|||
return date.getTime(); |
|||
}) |
|||
) |
|||
.map((timestamp) => { |
|||
return new Date(timestamp); |
|||
}) |
|||
.sort((a, b) => { |
|||
return a.getTime() - b.getTime(); |
|||
}); |
|||
|
|||
let exchangeRatesByCurrency = |
|||
await this.exchangeRateDataService.getExchangeRatesByCurrency({ |
|||
currencies: uniq(Object.values(currencies)), |
|||
endDate: endOfDay(endDate), |
|||
startDate: this.getStartDate(), |
|||
targetCurrency: this.currency |
|||
}); |
|||
|
|||
const { |
|||
dataProviderInfos, |
|||
errors: currentRateErrors, |
|||
values: marketSymbols |
|||
} = await this.currentRateService.getValues({ |
|||
dataGatheringItems, |
|||
dateQuery: { |
|||
in: dates |
|||
} |
|||
}); |
|||
|
|||
this.dataProviderInfos = dataProviderInfos; |
|||
|
|||
const marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
} = {}; |
|||
|
|||
for (const marketSymbol of marketSymbols) { |
|||
const date = format(marketSymbol.date, DATE_FORMAT); |
|||
|
|||
if (!marketSymbolMap[date]) { |
|||
marketSymbolMap[date] = {}; |
|||
} |
|||
|
|||
if (marketSymbol.marketPrice) { |
|||
marketSymbolMap[date][marketSymbol.symbol] = new Big( |
|||
marketSymbol.marketPrice |
|||
); |
|||
} |
|||
} |
|||
|
|||
const endDateString = format(endDate, DATE_FORMAT); |
|||
|
|||
if (firstIndex > 0) { |
|||
firstIndex--; |
|||
} |
|||
|
|||
const positions: TimelinePosition[] = []; |
|||
let hasAnySymbolMetricsErrors = false; |
|||
|
|||
const errors: ResponseError['errors'] = []; |
|||
|
|||
for (const item of lastTransactionPoint.items) { |
|||
const marketPriceInBaseCurrency = ( |
|||
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice |
|||
).mul( |
|||
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ |
|||
endDateString |
|||
] |
|||
); |
|||
|
|||
const { |
|||
grossPerformance, |
|||
grossPerformancePercentage, |
|||
grossPerformancePercentageWithCurrencyEffect, |
|||
grossPerformanceWithCurrencyEffect, |
|||
hasErrors, |
|||
netPerformance, |
|||
netPerformancePercentage, |
|||
netPerformancePercentageWithCurrencyEffect, |
|||
netPerformanceWithCurrencyEffect, |
|||
timeWeightedInvestment, |
|||
timeWeightedInvestmentWithCurrencyEffect, |
|||
totalDividend, |
|||
totalDividendInBaseCurrency, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect |
|||
} = this.getSymbolMetrics({ |
|||
marketSymbolMap, |
|||
start, |
|||
dataSource: item.dataSource, |
|||
end: endDate, |
|||
exchangeRates: |
|||
exchangeRatesByCurrency[`${item.currency}${this.currency}`], |
|||
symbol: item.symbol |
|||
}); |
|||
|
|||
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; |
|||
|
|||
positions.push({ |
|||
dividend: totalDividend, |
|||
dividendInBaseCurrency: totalDividendInBaseCurrency, |
|||
timeWeightedInvestment, |
|||
timeWeightedInvestmentWithCurrencyEffect, |
|||
averagePrice: item.averagePrice, |
|||
currency: item.currency, |
|||
dataSource: item.dataSource, |
|||
fee: item.fee, |
|||
firstBuyDate: item.firstBuyDate, |
|||
grossPerformance: !hasErrors ? grossPerformance ?? null : null, |
|||
grossPerformancePercentage: !hasErrors |
|||
? grossPerformancePercentage ?? null |
|||
: null, |
|||
grossPerformancePercentageWithCurrencyEffect: !hasErrors |
|||
? grossPerformancePercentageWithCurrencyEffect ?? null |
|||
: null, |
|||
grossPerformanceWithCurrencyEffect: !hasErrors |
|||
? grossPerformanceWithCurrencyEffect ?? null |
|||
: null, |
|||
investment: totalInvestment, |
|||
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, |
|||
marketPrice: |
|||
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, |
|||
marketPriceInBaseCurrency: |
|||
marketPriceInBaseCurrency?.toNumber() ?? null, |
|||
netPerformance: !hasErrors ? netPerformance ?? null : null, |
|||
netPerformancePercentage: !hasErrors |
|||
? netPerformancePercentage ?? null |
|||
: null, |
|||
netPerformancePercentageWithCurrencyEffect: !hasErrors |
|||
? netPerformancePercentageWithCurrencyEffect ?? null |
|||
: null, |
|||
netPerformanceWithCurrencyEffect: !hasErrors |
|||
? netPerformanceWithCurrencyEffect ?? null |
|||
: null, |
|||
quantity: item.quantity, |
|||
symbol: item.symbol, |
|||
tags: item.tags, |
|||
transactionCount: item.transactionCount, |
|||
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( |
|||
item.quantity |
|||
) |
|||
}); |
|||
|
|||
if ( |
|||
(hasErrors || |
|||
currentRateErrors.find(({ dataSource, symbol }) => { |
|||
return dataSource === item.dataSource && symbol === item.symbol; |
|||
})) && |
|||
item.investment.gt(0) |
|||
) { |
|||
errors.push({ dataSource: item.dataSource, symbol: item.symbol }); |
|||
} |
|||
} |
|||
|
|||
const overall = this.calculateOverallPerformance(positions); |
|||
|
|||
return { |
|||
...overall, |
|||
errors, |
|||
positions, |
|||
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors |
|||
}; |
|||
} |
|||
|
|||
public getDataProviderInfos() { |
|||
return this.dataProviderInfos; |
|||
} |
|||
|
|||
public getInvestments(): { date: string; investment: Big }[] { |
|||
if (this.transactionPoints.length === 0) { |
|||
return []; |
|||
} |
|||
|
|||
return this.transactionPoints.map((transactionPoint) => { |
|||
return { |
|||
date: transactionPoint.date, |
|||
investment: transactionPoint.items.reduce( |
|||
(investment, transactionPointSymbol) => |
|||
investment.plus(transactionPointSymbol.investment), |
|||
new Big(0) |
|||
) |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public getInvestmentsByGroup({ |
|||
data, |
|||
groupBy |
|||
}: { |
|||
data: HistoricalDataItem[]; |
|||
groupBy: GroupBy; |
|||
}): InvestmentItem[] { |
|||
const groupedData: { [dateGroup: string]: Big } = {}; |
|||
|
|||
for (const { date, investmentValueWithCurrencyEffect } of data) { |
|||
const dateGroup = |
|||
groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4); |
|||
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus( |
|||
investmentValueWithCurrencyEffect |
|||
); |
|||
} |
|||
|
|||
return Object.keys(groupedData).map((dateGroup) => ({ |
|||
date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`, |
|||
investment: groupedData[dateGroup].toNumber() |
|||
})); |
|||
} |
|||
|
|||
public getStartDate() { |
|||
return this.transactionPoints.length > 0 |
|||
? parseDate(this.transactionPoints[0].date) |
|||
: new Date(); |
|||
} |
|||
|
|||
protected abstract getSymbolMetrics({ |
|||
dataSource, |
|||
end, |
|||
exchangeRates, |
|||
isChartMode, |
|||
marketSymbolMap, |
|||
start, |
|||
step, |
|||
symbol |
|||
}: { |
|||
end: Date; |
|||
exchangeRates: { [dateString: string]: number }; |
|||
isChartMode?: boolean; |
|||
marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
}; |
|||
start: Date; |
|||
step?: number; |
|||
} & UniqueAsset): SymbolMetrics; |
|||
|
|||
public getTransactionPoints() { |
|||
return this.transactionPoints; |
|||
} |
|||
|
|||
private computeTransactionPoints() { |
|||
this.transactionPoints = []; |
|||
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; |
|||
|
|||
let lastDate: string = null; |
|||
let lastTransactionPoint: TransactionPoint = null; |
|||
|
|||
for (const { |
|||
fee, |
|||
date, |
|||
quantity, |
|||
SymbolProfile, |
|||
tags, |
|||
type, |
|||
unitPrice |
|||
} of this.orders) { |
|||
let currentTransactionPointItem: TransactionPointSymbol; |
|||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; |
|||
|
|||
const factor = getFactor(type); |
|||
|
|||
if (oldAccumulatedSymbol) { |
|||
let investment = oldAccumulatedSymbol.investment; |
|||
|
|||
const newQuantity = quantity |
|||
.mul(factor) |
|||
.plus(oldAccumulatedSymbol.quantity); |
|||
|
|||
if (type === 'BUY') { |
|||
investment = oldAccumulatedSymbol.investment.plus( |
|||
quantity.mul(unitPrice) |
|||
); |
|||
} else if (type === 'SELL') { |
|||
investment = oldAccumulatedSymbol.investment.minus( |
|||
quantity.mul(oldAccumulatedSymbol.averagePrice) |
|||
); |
|||
} |
|||
|
|||
currentTransactionPointItem = { |
|||
investment, |
|||
tags, |
|||
averagePrice: newQuantity.gt(0) |
|||
? investment.div(newQuantity) |
|||
: new Big(0), |
|||
currency: SymbolProfile.currency, |
|||
dataSource: SymbolProfile.dataSource, |
|||
dividend: new Big(0), |
|||
fee: fee.plus(oldAccumulatedSymbol.fee), |
|||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, |
|||
quantity: newQuantity, |
|||
symbol: SymbolProfile.symbol, |
|||
transactionCount: oldAccumulatedSymbol.transactionCount + 1 |
|||
}; |
|||
} else { |
|||
currentTransactionPointItem = { |
|||
fee, |
|||
tags, |
|||
averagePrice: unitPrice, |
|||
currency: SymbolProfile.currency, |
|||
dataSource: SymbolProfile.dataSource, |
|||
dividend: new Big(0), |
|||
firstBuyDate: date, |
|||
investment: unitPrice.mul(quantity).mul(factor), |
|||
quantity: quantity.mul(factor), |
|||
symbol: SymbolProfile.symbol, |
|||
transactionCount: 1 |
|||
}; |
|||
} |
|||
|
|||
symbols[SymbolProfile.symbol] = currentTransactionPointItem; |
|||
|
|||
const items = lastTransactionPoint?.items ?? []; |
|||
|
|||
const newItems = items.filter(({ symbol }) => { |
|||
return symbol !== SymbolProfile.symbol; |
|||
}); |
|||
|
|||
newItems.push(currentTransactionPointItem); |
|||
|
|||
newItems.sort((a, b) => { |
|||
return a.symbol?.localeCompare(b.symbol); |
|||
}); |
|||
|
|||
if (lastDate !== date || lastTransactionPoint === null) { |
|||
lastTransactionPoint = { |
|||
date, |
|||
items: newItems |
|||
}; |
|||
|
|||
this.transactionPoints.push(lastTransactionPoint); |
|||
} else { |
|||
lastTransactionPoint.items = newItems; |
|||
} |
|||
|
|||
lastDate = date; |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue