|
@ -1,7 +1,7 @@ |
|
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; |
|
|
|
|
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; |
|
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; |
|
|
|
|
|
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; |
|
|
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; |
|
|
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; |
|
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; |
|
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; |
|
|
import { |
|
|
import { |
|
@ -11,7 +11,12 @@ import { |
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
|
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
|
|
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; |
|
|
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; |
|
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
|
|
import { |
|
|
|
|
|
DATE_FORMAT, |
|
|
|
|
|
getSum, |
|
|
|
|
|
parseDate, |
|
|
|
|
|
resetHours |
|
|
|
|
|
} from '@ghostfolio/common/helper'; |
|
|
import { |
|
|
import { |
|
|
DataProviderInfo, |
|
|
DataProviderInfo, |
|
|
HistoricalDataItem, |
|
|
HistoricalDataItem, |
|
@ -44,18 +49,24 @@ export abstract class PortfolioCalculator { |
|
|
private currency: string; |
|
|
private currency: string; |
|
|
private currentRateService: CurrentRateService; |
|
|
private currentRateService: CurrentRateService; |
|
|
private dataProviderInfos: DataProviderInfo[]; |
|
|
private dataProviderInfos: DataProviderInfo[]; |
|
|
|
|
|
private endDate: Date; |
|
|
private exchangeRateDataService: ExchangeRateDataService; |
|
|
private exchangeRateDataService: ExchangeRateDataService; |
|
|
|
|
|
private snapshot: PortfolioSnapshot; |
|
|
|
|
|
private snapshotPromise: Promise<void>; |
|
|
|
|
|
private startDate: Date; |
|
|
private transactionPoints: TransactionPoint[]; |
|
|
private transactionPoints: TransactionPoint[]; |
|
|
|
|
|
|
|
|
public constructor({ |
|
|
public constructor({ |
|
|
activities, |
|
|
activities, |
|
|
currency, |
|
|
currency, |
|
|
currentRateService, |
|
|
currentRateService, |
|
|
|
|
|
dateRange, |
|
|
exchangeRateDataService |
|
|
exchangeRateDataService |
|
|
}: { |
|
|
}: { |
|
|
activities: Activity[]; |
|
|
activities: Activity[]; |
|
|
currency: string; |
|
|
currency: string; |
|
|
currentRateService: CurrentRateService; |
|
|
currentRateService: CurrentRateService; |
|
|
|
|
|
dateRange: DateRange; |
|
|
exchangeRateDataService: ExchangeRateDataService; |
|
|
exchangeRateDataService: ExchangeRateDataService; |
|
|
}) { |
|
|
}) { |
|
|
this.currency = currency; |
|
|
this.currency = currency; |
|
@ -79,12 +90,270 @@ export abstract class PortfolioCalculator { |
|
|
return a.date?.localeCompare(b.date); |
|
|
return a.date?.localeCompare(b.date); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { endDate, startDate } = getInterval(dateRange); |
|
|
|
|
|
|
|
|
|
|
|
this.endDate = endDate; |
|
|
|
|
|
this.startDate = startDate; |
|
|
|
|
|
|
|
|
this.computeTransactionPoints(); |
|
|
this.computeTransactionPoints(); |
|
|
|
|
|
|
|
|
|
|
|
this.snapshotPromise = this.initialize(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
protected abstract calculateOverallPerformance( |
|
|
protected abstract calculateOverallPerformance( |
|
|
positions: TimelinePosition[] |
|
|
positions: TimelinePosition[] |
|
|
): CurrentPositions; |
|
|
): PortfolioSnapshot; |
|
|
|
|
|
|
|
|
|
|
|
public async computeSnapshot( |
|
|
|
|
|
start: Date, |
|
|
|
|
|
end?: Date |
|
|
|
|
|
): Promise<PortfolioSnapshot> { |
|
|
|
|
|
const lastTransactionPoint = last(this.transactionPoints); |
|
|
|
|
|
|
|
|
|
|
|
let endDate = end; |
|
|
|
|
|
|
|
|
|
|
|
if (!endDate) { |
|
|
|
|
|
endDate = new Date(Date.now()); |
|
|
|
|
|
|
|
|
|
|
|
if (lastTransactionPoint) { |
|
|
|
|
|
endDate = max([endDate, parseDate(lastTransactionPoint.date)]); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const transactionPoints = this.transactionPoints?.filter(({ date }) => { |
|
|
|
|
|
return isBefore(parseDate(date), endDate); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (!transactionPoints.length) { |
|
|
|
|
|
return { |
|
|
|
|
|
currentValueInBaseCurrency: new Big(0), |
|
|
|
|
|
grossPerformance: new Big(0), |
|
|
|
|
|
grossPerformancePercentage: new Big(0), |
|
|
|
|
|
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|
|
|
|
|
grossPerformanceWithCurrencyEffect: new Big(0), |
|
|
|
|
|
hasErrors: false, |
|
|
|
|
|
netPerformance: new Big(0), |
|
|
|
|
|
netPerformancePercentage: new Big(0), |
|
|
|
|
|
netPerformancePercentageWithCurrencyEffect: new Big(0), |
|
|
|
|
|
netPerformanceWithCurrencyEffect: new Big(0), |
|
|
|
|
|
positions: [], |
|
|
|
|
|
totalFeesWithCurrencyEffect: new Big(0), |
|
|
|
|
|
totalInterestWithCurrencyEffect: new Big(0), |
|
|
|
|
|
totalInvestment: new Big(0), |
|
|
|
|
|
totalInvestmentWithCurrencyEffect: new Big(0) |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const currencies: { [symbol: string]: string } = {}; |
|
|
|
|
|
const dataGatheringItems: IDataGatheringItem[] = []; |
|
|
|
|
|
let dates: Date[] = []; |
|
|
|
|
|
let firstIndex = transactionPoints.length; |
|
|
|
|
|
let firstTransactionPoint: TransactionPoint = null; |
|
|
|
|
|
|
|
|
|
|
|
dates.push(resetHours(start)); |
|
|
|
|
|
|
|
|
|
|
|
for (const { currency, dataSource, symbol } of transactionPoints[ |
|
|
|
|
|
firstIndex - 1 |
|
|
|
|
|
].items) { |
|
|
|
|
|
dataGatheringItems.push({ |
|
|
|
|
|
dataSource, |
|
|
|
|
|
symbol |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
currencies[symbol] = currency; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < transactionPoints.length; i++) { |
|
|
|
|
|
if ( |
|
|
|
|
|
!isBefore(parseDate(transactionPoints[i].date), start) && |
|
|
|
|
|
firstTransactionPoint === null |
|
|
|
|
|
) { |
|
|
|
|
|
firstTransactionPoint = transactionPoints[i]; |
|
|
|
|
|
firstIndex = i; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (firstTransactionPoint !== null) { |
|
|
|
|
|
dates.push(resetHours(parseDate(transactionPoints[i].date))); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
dates.push(resetHours(endDate)); |
|
|
|
|
|
|
|
|
|
|
|
// Add dates of last week for fallback
|
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 7)); |
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 6)); |
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 5)); |
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 4)); |
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 3)); |
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 2)); |
|
|
|
|
|
dates.push(subDays(resetHours(new Date()), 1)); |
|
|
|
|
|
dates.push(resetHours(new Date())); |
|
|
|
|
|
|
|
|
|
|
|
dates = uniq( |
|
|
|
|
|
dates.map((date) => { |
|
|
|
|
|
return date.getTime(); |
|
|
|
|
|
}) |
|
|
|
|
|
) |
|
|
|
|
|
.map((timestamp) => { |
|
|
|
|
|
return new Date(timestamp); |
|
|
|
|
|
}) |
|
|
|
|
|
.sort((a, b) => { |
|
|
|
|
|
return a.getTime() - b.getTime(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
let exchangeRatesByCurrency = |
|
|
|
|
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({ |
|
|
|
|
|
currencies: uniq(Object.values(currencies)), |
|
|
|
|
|
endDate: endOfDay(endDate), |
|
|
|
|
|
startDate: this.getStartDate(), |
|
|
|
|
|
targetCurrency: this.currency |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
dataProviderInfos, |
|
|
|
|
|
errors: currentRateErrors, |
|
|
|
|
|
values: marketSymbols |
|
|
|
|
|
} = await this.currentRateService.getValues({ |
|
|
|
|
|
dataGatheringItems, |
|
|
|
|
|
dateQuery: { |
|
|
|
|
|
in: dates |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.dataProviderInfos = dataProviderInfos; |
|
|
|
|
|
|
|
|
|
|
|
const marketSymbolMap: { |
|
|
|
|
|
[date: string]: { [symbol: string]: Big }; |
|
|
|
|
|
} = {}; |
|
|
|
|
|
|
|
|
|
|
|
for (const marketSymbol of marketSymbols) { |
|
|
|
|
|
const date = format(marketSymbol.date, DATE_FORMAT); |
|
|
|
|
|
|
|
|
|
|
|
if (!marketSymbolMap[date]) { |
|
|
|
|
|
marketSymbolMap[date] = {}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (marketSymbol.marketPrice) { |
|
|
|
|
|
marketSymbolMap[date][marketSymbol.symbol] = new Big( |
|
|
|
|
|
marketSymbol.marketPrice |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const endDateString = format(endDate, DATE_FORMAT); |
|
|
|
|
|
|
|
|
|
|
|
if (firstIndex > 0) { |
|
|
|
|
|
firstIndex--; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const positions: TimelinePosition[] = []; |
|
|
|
|
|
let hasAnySymbolMetricsErrors = false; |
|
|
|
|
|
|
|
|
|
|
|
const errors: ResponseError['errors'] = []; |
|
|
|
|
|
|
|
|
|
|
|
for (const item of lastTransactionPoint.items) { |
|
|
|
|
|
const marketPriceInBaseCurrency = ( |
|
|
|
|
|
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice |
|
|
|
|
|
).mul( |
|
|
|
|
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ |
|
|
|
|
|
endDateString |
|
|
|
|
|
] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
grossPerformance, |
|
|
|
|
|
grossPerformancePercentage, |
|
|
|
|
|
grossPerformancePercentageWithCurrencyEffect, |
|
|
|
|
|
grossPerformanceWithCurrencyEffect, |
|
|
|
|
|
hasErrors, |
|
|
|
|
|
netPerformance, |
|
|
|
|
|
netPerformancePercentage, |
|
|
|
|
|
netPerformancePercentageWithCurrencyEffect, |
|
|
|
|
|
netPerformanceWithCurrencyEffect, |
|
|
|
|
|
timeWeightedInvestment, |
|
|
|
|
|
timeWeightedInvestmentWithCurrencyEffect, |
|
|
|
|
|
totalDividend, |
|
|
|
|
|
totalDividendInBaseCurrency, |
|
|
|
|
|
totalInvestment, |
|
|
|
|
|
totalInvestmentWithCurrencyEffect |
|
|
|
|
|
} = this.getSymbolMetrics({ |
|
|
|
|
|
marketSymbolMap, |
|
|
|
|
|
start, |
|
|
|
|
|
dataSource: item.dataSource, |
|
|
|
|
|
end: endDate, |
|
|
|
|
|
exchangeRates: |
|
|
|
|
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`], |
|
|
|
|
|
symbol: item.symbol |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; |
|
|
|
|
|
|
|
|
|
|
|
positions.push({ |
|
|
|
|
|
dividend: totalDividend, |
|
|
|
|
|
dividendInBaseCurrency: totalDividendInBaseCurrency, |
|
|
|
|
|
timeWeightedInvestment, |
|
|
|
|
|
timeWeightedInvestmentWithCurrencyEffect, |
|
|
|
|
|
averagePrice: item.averagePrice, |
|
|
|
|
|
currency: item.currency, |
|
|
|
|
|
dataSource: item.dataSource, |
|
|
|
|
|
fee: item.fee, |
|
|
|
|
|
firstBuyDate: item.firstBuyDate, |
|
|
|
|
|
grossPerformance: !hasErrors ? grossPerformance ?? null : null, |
|
|
|
|
|
grossPerformancePercentage: !hasErrors |
|
|
|
|
|
? grossPerformancePercentage ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
grossPerformancePercentageWithCurrencyEffect: !hasErrors |
|
|
|
|
|
? grossPerformancePercentageWithCurrencyEffect ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
grossPerformanceWithCurrencyEffect: !hasErrors |
|
|
|
|
|
? grossPerformanceWithCurrencyEffect ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
investment: totalInvestment, |
|
|
|
|
|
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, |
|
|
|
|
|
marketPrice: |
|
|
|
|
|
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, |
|
|
|
|
|
marketPriceInBaseCurrency: |
|
|
|
|
|
marketPriceInBaseCurrency?.toNumber() ?? null, |
|
|
|
|
|
netPerformance: !hasErrors ? netPerformance ?? null : null, |
|
|
|
|
|
netPerformancePercentage: !hasErrors |
|
|
|
|
|
? netPerformancePercentage ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
netPerformancePercentageWithCurrencyEffect: !hasErrors |
|
|
|
|
|
? netPerformancePercentageWithCurrencyEffect ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
netPerformanceWithCurrencyEffect: !hasErrors |
|
|
|
|
|
? netPerformanceWithCurrencyEffect ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
quantity: item.quantity, |
|
|
|
|
|
symbol: item.symbol, |
|
|
|
|
|
tags: item.tags, |
|
|
|
|
|
transactionCount: item.transactionCount, |
|
|
|
|
|
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( |
|
|
|
|
|
item.quantity |
|
|
|
|
|
) |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
(hasErrors || |
|
|
|
|
|
currentRateErrors.find(({ dataSource, symbol }) => { |
|
|
|
|
|
return dataSource === item.dataSource && symbol === item.symbol; |
|
|
|
|
|
})) && |
|
|
|
|
|
item.investment.gt(0) |
|
|
|
|
|
) { |
|
|
|
|
|
errors.push({ dataSource: item.dataSource, symbol: item.symbol }); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const overall = this.calculateOverallPerformance(positions); |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
...overall, |
|
|
|
|
|
errors, |
|
|
|
|
|
positions, |
|
|
|
|
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, |
|
|
|
|
|
totalInterestWithCurrencyEffect: lastTransactionPoint.interest |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
public async getChart({ |
|
|
public async getChart({ |
|
|
dateRange = 'max', |
|
|
dateRange = 'max', |
|
@ -380,256 +649,30 @@ export abstract class PortfolioCalculator { |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async getCurrentPositions( |
|
|
public getDataProviderInfos() { |
|
|
start: Date, |
|
|
return this.dataProviderInfos; |
|
|
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
|
|
|
public async getDividendInBaseCurrency() { |
|
|
dates.push(subDays(resetHours(new Date()), 7)); |
|
|
await this.snapshotPromise; |
|
|
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( |
|
|
return getSum( |
|
|
dates.map((date) => { |
|
|
this.snapshot.positions.map(({ dividendInBaseCurrency }) => { |
|
|
return date.getTime(); |
|
|
return dividendInBaseCurrency; |
|
|
}) |
|
|
|
|
|
) |
|
|
|
|
|
.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); |
|
|
public async getFeesInBaseCurrency() { |
|
|
|
|
|
await this.snapshotPromise; |
|
|
|
|
|
|
|
|
return { |
|
|
return this.snapshot.totalFeesWithCurrencyEffect; |
|
|
...overall, |
|
|
|
|
|
errors, |
|
|
|
|
|
positions, |
|
|
|
|
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public getDataProviderInfos() { |
|
|
public async getInterestInBaseCurrency() { |
|
|
return this.dataProviderInfos; |
|
|
await this.snapshotPromise; |
|
|
|
|
|
|
|
|
|
|
|
return this.snapshot.totalInterestWithCurrencyEffect; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public getInvestments(): { date: string; investment: Big }[] { |
|
|
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() { |
|
|
public getStartDate() { |
|
|
return this.transactionPoints.length > 0 |
|
|
return this.transactionPoints.length > 0 |
|
|
? parseDate(this.transactionPoints[0].date) |
|
|
? parseDate(this.transactionPoints[0].date) |
|
@ -718,6 +767,13 @@ export abstract class PortfolioCalculator { |
|
|
type, |
|
|
type, |
|
|
unitPrice |
|
|
unitPrice |
|
|
} of this.orders) { |
|
|
} of this.orders) { |
|
|
|
|
|
if ( |
|
|
|
|
|
// TODO
|
|
|
|
|
|
['ITEM', 'LIABILITY'].includes(type) |
|
|
|
|
|
) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
let currentTransactionPointItem: TransactionPointSymbol; |
|
|
let currentTransactionPointItem: TransactionPointSymbol; |
|
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; |
|
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; |
|
|
|
|
|
|
|
@ -790,18 +846,39 @@ export abstract class PortfolioCalculator { |
|
|
return a.symbol?.localeCompare(b.symbol); |
|
|
return a.symbol?.localeCompare(b.symbol); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
let fees = new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
if (type === 'FEE') { |
|
|
|
|
|
fees = fee; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let interest = new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
if (type === 'INTEREST') { |
|
|
|
|
|
interest = quantity.mul(unitPrice); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (lastDate !== date || lastTransactionPoint === null) { |
|
|
if (lastDate !== date || lastTransactionPoint === null) { |
|
|
lastTransactionPoint = { |
|
|
lastTransactionPoint = { |
|
|
date, |
|
|
date, |
|
|
|
|
|
fees, |
|
|
|
|
|
interest, |
|
|
items: newItems |
|
|
items: newItems |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
this.transactionPoints.push(lastTransactionPoint); |
|
|
this.transactionPoints.push(lastTransactionPoint); |
|
|
} else { |
|
|
} else { |
|
|
|
|
|
lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees); |
|
|
|
|
|
lastTransactionPoint.interest = |
|
|
|
|
|
lastTransactionPoint.interest.plus(interest); |
|
|
lastTransactionPoint.items = newItems; |
|
|
lastTransactionPoint.items = newItems; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
lastDate = date; |
|
|
lastDate = date; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async initialize() { |
|
|
|
|
|
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|