|
|
@ -37,13 +37,15 @@ import { |
|
|
|
isBefore, |
|
|
|
isSameDay, |
|
|
|
max, |
|
|
|
min, |
|
|
|
subDays |
|
|
|
} from 'date-fns'; |
|
|
|
import { last, uniq, uniqBy } from 'lodash'; |
|
|
|
import { first, last, uniq, uniqBy } from 'lodash'; |
|
|
|
|
|
|
|
export abstract class PortfolioCalculator { |
|
|
|
protected static readonly ENABLE_LOGGING = false; |
|
|
|
|
|
|
|
protected accountBalanceItems: HistoricalDataItem[]; |
|
|
|
protected orders: PortfolioOrder[]; |
|
|
|
|
|
|
|
private currency: string; |
|
|
@ -57,18 +59,21 @@ export abstract class PortfolioCalculator { |
|
|
|
private transactionPoints: TransactionPoint[]; |
|
|
|
|
|
|
|
public constructor({ |
|
|
|
accountBalanceItems, |
|
|
|
activities, |
|
|
|
currency, |
|
|
|
currentRateService, |
|
|
|
dateRange, |
|
|
|
exchangeRateDataService |
|
|
|
}: { |
|
|
|
accountBalanceItems: HistoricalDataItem[]; |
|
|
|
activities: Activity[]; |
|
|
|
currency: string; |
|
|
|
currentRateService: CurrentRateService; |
|
|
|
dateRange: DateRange; |
|
|
|
exchangeRateDataService: ExchangeRateDataService; |
|
|
|
}) { |
|
|
|
this.accountBalanceItems = accountBalanceItems; |
|
|
|
this.currency = currency; |
|
|
|
this.currentRateService = currentRateService; |
|
|
|
this.exchangeRateDataService = exchangeRateDataService; |
|
|
@ -383,10 +388,6 @@ export abstract class PortfolioCalculator { |
|
|
|
dateRange?: DateRange; |
|
|
|
withDataDecimation?: boolean; |
|
|
|
}): Promise<HistoricalDataItem[]> { |
|
|
|
if (this.getTransactionPoints().length === 0) { |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
const { endDate, startDate } = getInterval(dateRange, this.getStartDate()); |
|
|
|
|
|
|
|
const daysInMarket = differenceInDays(endDate, startDate) + 1; |
|
|
@ -485,6 +486,7 @@ export abstract class PortfolioCalculator { |
|
|
|
investmentValueWithCurrencyEffect: Big; |
|
|
|
totalCurrentValue: Big; |
|
|
|
totalCurrentValueWithCurrencyEffect: Big; |
|
|
|
totalAccountBalanceWithCurrencyEffect: Big; |
|
|
|
totalInvestmentValue: Big; |
|
|
|
totalInvestmentValueWithCurrencyEffect: Big; |
|
|
|
totalNetPerformanceValue: Big; |
|
|
@ -544,9 +546,24 @@ export abstract class PortfolioCalculator { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
let lastDate = format(this.startDate, DATE_FORMAT); |
|
|
|
|
|
|
|
for (const currentDate of dates) { |
|
|
|
const dateString = format(currentDate, DATE_FORMAT); |
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString] = { |
|
|
|
investmentValueWithCurrencyEffect: new Big(0), |
|
|
|
totalAccountBalanceWithCurrencyEffect: new Big(0), |
|
|
|
totalCurrentValue: new Big(0), |
|
|
|
totalCurrentValueWithCurrencyEffect: new Big(0), |
|
|
|
totalInvestmentValue: new Big(0), |
|
|
|
totalInvestmentValueWithCurrencyEffect: new Big(0), |
|
|
|
totalNetPerformanceValue: new Big(0), |
|
|
|
totalNetPerformanceValueWithCurrencyEffect: new Big(0), |
|
|
|
totalTimeWeightedInvestmentValue: new Big(0), |
|
|
|
totalTimeWeightedInvestmentValueWithCurrencyEffect: new Big(0) |
|
|
|
}; |
|
|
|
|
|
|
|
for (const symbol of Object.keys(valuesBySymbol)) { |
|
|
|
const symbolValues = valuesBySymbol[symbol]; |
|
|
|
|
|
|
@ -584,49 +601,94 @@ export abstract class PortfolioCalculator { |
|
|
|
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) |
|
|
|
}; |
|
|
|
accumulatedValuesByDate[dateString].investmentValueWithCurrencyEffect = |
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].investmentValueWithCurrencyEffect.add( |
|
|
|
investmentValueWithCurrencyEffect |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalCurrentValue = |
|
|
|
accumulatedValuesByDate[dateString].totalCurrentValue.add( |
|
|
|
currentValue |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalCurrentValueWithCurrencyEffect = accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalCurrentValueWithCurrencyEffect.add( |
|
|
|
currentValueWithCurrencyEffect |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalInvestmentValue = |
|
|
|
accumulatedValuesByDate[dateString].totalInvestmentValue.add( |
|
|
|
investmentValueAccumulated |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalInvestmentValueWithCurrencyEffect = accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalInvestmentValueWithCurrencyEffect.add( |
|
|
|
investmentValueAccumulatedWithCurrencyEffect |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalNetPerformanceValue = |
|
|
|
accumulatedValuesByDate[dateString].totalNetPerformanceValue.add( |
|
|
|
netPerformanceValue |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalNetPerformanceValueWithCurrencyEffect = accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalNetPerformanceValueWithCurrencyEffect.add( |
|
|
|
netPerformanceValueWithCurrencyEffect |
|
|
|
); |
|
|
|
|
|
|
|
accumulatedValuesByDate[dateString].totalTimeWeightedInvestmentValue = |
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalTimeWeightedInvestmentValue.add(timeWeightedInvestmentValue); |
|
|
|
|
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalTimeWeightedInvestmentValueWithCurrencyEffect = |
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalTimeWeightedInvestmentValueWithCurrencyEffect.add( |
|
|
|
timeWeightedInvestmentValueWithCurrencyEffect |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
if ( |
|
|
|
this.accountBalanceItems.some(({ date }) => { |
|
|
|
return date === dateString; |
|
|
|
}) |
|
|
|
) { |
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalAccountBalanceWithCurrencyEffect = new Big( |
|
|
|
this.accountBalanceItems.find(({ date }) => { |
|
|
|
return date === dateString; |
|
|
|
}).value |
|
|
|
); |
|
|
|
} else { |
|
|
|
accumulatedValuesByDate[ |
|
|
|
dateString |
|
|
|
].totalAccountBalanceWithCurrencyEffect = |
|
|
|
accumulatedValuesByDate[lastDate] |
|
|
|
?.totalAccountBalanceWithCurrencyEffect ?? new Big(0); |
|
|
|
} |
|
|
|
|
|
|
|
lastDate = dateString; |
|
|
|
} |
|
|
|
|
|
|
|
return Object.entries(accumulatedValuesByDate).map(([date, values]) => { |
|
|
|
const { |
|
|
|
investmentValueWithCurrencyEffect, |
|
|
|
totalAccountBalanceWithCurrencyEffect, |
|
|
|
totalCurrentValue, |
|
|
|
totalCurrentValueWithCurrencyEffect, |
|
|
|
totalInvestmentValue, |
|
|
@ -661,6 +723,11 @@ export abstract class PortfolioCalculator { |
|
|
|
netPerformance: totalNetPerformanceValue.toNumber(), |
|
|
|
netPerformanceWithCurrencyEffect: |
|
|
|
totalNetPerformanceValueWithCurrencyEffect.toNumber(), |
|
|
|
// TODO: Add valuables
|
|
|
|
netWorth: totalCurrentValueWithCurrencyEffect |
|
|
|
.plus(totalAccountBalanceWithCurrencyEffect) |
|
|
|
.toNumber(), |
|
|
|
totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(), |
|
|
|
totalInvestment: totalInvestmentValue.toNumber(), |
|
|
|
totalInvestmentValueWithCurrencyEffect: |
|
|
|
totalInvestmentValueWithCurrencyEffect.toNumber(), |
|
|
@ -749,9 +816,30 @@ export abstract class PortfolioCalculator { |
|
|
|
} |
|
|
|
|
|
|
|
public getStartDate() { |
|
|
|
return this.transactionPoints.length > 0 |
|
|
|
? parseDate(this.transactionPoints[0].date) |
|
|
|
let firstAccountBalanceDate: Date; |
|
|
|
let firstActivityDate: Date; |
|
|
|
|
|
|
|
try { |
|
|
|
const firstAccountBalanceDateString = first( |
|
|
|
this.accountBalanceItems |
|
|
|
)?.date; |
|
|
|
firstAccountBalanceDate = firstAccountBalanceDateString |
|
|
|
? parseDate(firstAccountBalanceDateString) |
|
|
|
: new Date(); |
|
|
|
} catch (error) { |
|
|
|
firstAccountBalanceDate = new Date(); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const firstActivityDateString = this.transactionPoints[0].date; |
|
|
|
firstActivityDate = firstActivityDateString |
|
|
|
? parseDate(firstActivityDateString) |
|
|
|
: new Date(); |
|
|
|
} catch (error) { |
|
|
|
firstActivityDate = new Date(); |
|
|
|
} |
|
|
|
|
|
|
|
return min([firstAccountBalanceDate, firstActivityDate]); |
|
|
|
} |
|
|
|
|
|
|
|
protected abstract getSymbolMetrics({ |
|
|
|