mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Portfolio calculator rework Co-authored-by: Reto Kaul <retokaul@sublimd.com>pull/650/head
gizmodus
3 years ago
committed by
GitHub
7 changed files with 2150 additions and 40 deletions
@ -0,0 +1,5 @@ |
|||||
|
import { PortfolioOrder } from './portfolio-order.interface'; |
||||
|
|
||||
|
export interface PortfolioOrderItem extends PortfolioOrder { |
||||
|
itemType?: '' | 'start' | 'end'; |
||||
|
} |
@ -0,0 +1,897 @@ |
|||||
|
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; |
||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
||||
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
||||
|
import { TimelinePosition } from '@ghostfolio/common/interfaces'; |
||||
|
import { Logger } from '@nestjs/common'; |
||||
|
import { Type as TypeOfOrder } from '@prisma/client'; |
||||
|
import Big from 'big.js'; |
||||
|
import { |
||||
|
addDays, |
||||
|
addMilliseconds, |
||||
|
addMonths, |
||||
|
addYears, |
||||
|
differenceInDays, |
||||
|
endOfDay, |
||||
|
format, |
||||
|
isAfter, |
||||
|
isBefore, |
||||
|
max, |
||||
|
min |
||||
|
} from 'date-fns'; |
||||
|
import { first, flatten, isNumber, sortBy } from 'lodash'; |
||||
|
|
||||
|
import { CurrentRateService } from './current-rate.service'; |
||||
|
import { CurrentPositions } from './interfaces/current-positions.interface'; |
||||
|
import { GetValueObject } from './interfaces/get-value-object.interface'; |
||||
|
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface'; |
||||
|
import { PortfolioOrder } from './interfaces/portfolio-order.interface'; |
||||
|
import { TimelinePeriod } from './interfaces/timeline-period.interface'; |
||||
|
import { |
||||
|
Accuracy, |
||||
|
TimelineSpecification |
||||
|
} from './interfaces/timeline-specification.interface'; |
||||
|
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; |
||||
|
import { TransactionPoint } from './interfaces/transaction-point.interface'; |
||||
|
|
||||
|
export class PortfolioCalculatorNew { |
||||
|
private currency: string; |
||||
|
private currentRateService: CurrentRateService; |
||||
|
private orders: PortfolioOrder[]; |
||||
|
private transactionPoints: TransactionPoint[]; |
||||
|
|
||||
|
public constructor({ |
||||
|
currency, |
||||
|
currentRateService, |
||||
|
orders |
||||
|
}: { |
||||
|
currency: string; |
||||
|
currentRateService: CurrentRateService; |
||||
|
orders: PortfolioOrder[]; |
||||
|
}) { |
||||
|
this.currency = currency; |
||||
|
this.currentRateService = currentRateService; |
||||
|
this.orders = orders; |
||||
|
|
||||
|
this.orders.sort((a, b) => a.date.localeCompare(b.date)); |
||||
|
} |
||||
|
|
||||
|
public computeTransactionPoints() { |
||||
|
this.transactionPoints = []; |
||||
|
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; |
||||
|
|
||||
|
let lastDate: string = null; |
||||
|
let lastTransactionPoint: TransactionPoint = null; |
||||
|
for (const order of this.orders) { |
||||
|
const currentDate = order.date; |
||||
|
|
||||
|
let currentTransactionPointItem: TransactionPointSymbol; |
||||
|
const oldAccumulatedSymbol = symbols[order.symbol]; |
||||
|
|
||||
|
const factor = this.getFactor(order.type); |
||||
|
const unitPrice = new Big(order.unitPrice); |
||||
|
if (oldAccumulatedSymbol) { |
||||
|
const newQuantity = order.quantity |
||||
|
.mul(factor) |
||||
|
.plus(oldAccumulatedSymbol.quantity); |
||||
|
currentTransactionPointItem = { |
||||
|
currency: order.currency, |
||||
|
dataSource: order.dataSource, |
||||
|
fee: order.fee.plus(oldAccumulatedSymbol.fee), |
||||
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, |
||||
|
investment: newQuantity.eq(0) |
||||
|
? new Big(0) |
||||
|
: unitPrice |
||||
|
.mul(order.quantity) |
||||
|
.mul(factor) |
||||
|
.add(oldAccumulatedSymbol.investment), |
||||
|
quantity: newQuantity, |
||||
|
symbol: order.symbol, |
||||
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1 |
||||
|
}; |
||||
|
} else { |
||||
|
currentTransactionPointItem = { |
||||
|
currency: order.currency, |
||||
|
dataSource: order.dataSource, |
||||
|
fee: order.fee, |
||||
|
firstBuyDate: order.date, |
||||
|
investment: unitPrice.mul(order.quantity).mul(factor), |
||||
|
quantity: order.quantity.mul(factor), |
||||
|
symbol: order.symbol, |
||||
|
transactionCount: 1 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
symbols[order.symbol] = currentTransactionPointItem; |
||||
|
|
||||
|
const items = lastTransactionPoint?.items ?? []; |
||||
|
const newItems = items.filter( |
||||
|
(transactionPointItem) => transactionPointItem.symbol !== order.symbol |
||||
|
); |
||||
|
newItems.push(currentTransactionPointItem); |
||||
|
newItems.sort((a, b) => a.symbol.localeCompare(b.symbol)); |
||||
|
if (lastDate !== currentDate || lastTransactionPoint === null) { |
||||
|
lastTransactionPoint = { |
||||
|
date: currentDate, |
||||
|
items: newItems |
||||
|
}; |
||||
|
this.transactionPoints.push(lastTransactionPoint); |
||||
|
} else { |
||||
|
lastTransactionPoint.items = newItems; |
||||
|
} |
||||
|
lastDate = currentDate; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getAnnualizedPerformancePercent({ |
||||
|
daysInMarket, |
||||
|
netPerformancePercent |
||||
|
}: { |
||||
|
daysInMarket: number; |
||||
|
netPerformancePercent: Big; |
||||
|
}): Big { |
||||
|
if (isNumber(daysInMarket) && daysInMarket > 0) { |
||||
|
return netPerformancePercent.mul(daysInMarket).div(365); |
||||
|
} |
||||
|
|
||||
|
return new Big(0); |
||||
|
} |
||||
|
|
||||
|
public getTransactionPoints(): TransactionPoint[] { |
||||
|
return this.transactionPoints; |
||||
|
} |
||||
|
|
||||
|
public setTransactionPoints(transactionPoints: TransactionPoint[]) { |
||||
|
this.transactionPoints = transactionPoints; |
||||
|
} |
||||
|
|
||||
|
public async getCurrentPositions(start: Date): Promise<CurrentPositions> { |
||||
|
if (!this.transactionPoints?.length) { |
||||
|
return { |
||||
|
currentValue: new Big(0), |
||||
|
hasErrors: false, |
||||
|
grossPerformance: new Big(0), |
||||
|
grossPerformancePercentage: new Big(0), |
||||
|
netAnnualizedPerformance: new Big(0), |
||||
|
netPerformance: new Big(0), |
||||
|
netPerformancePercentage: new Big(0), |
||||
|
positions: [], |
||||
|
totalInvestment: new Big(0) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const lastTransactionPoint = |
||||
|
this.transactionPoints[this.transactionPoints.length - 1]; |
||||
|
|
||||
|
// use Date.now() to use the mock for today
|
||||
|
const today = new Date(Date.now()); |
||||
|
|
||||
|
let firstTransactionPoint: TransactionPoint = null; |
||||
|
let firstIndex = this.transactionPoints.length; |
||||
|
const dates = []; |
||||
|
const dataGatheringItems: IDataGatheringItem[] = []; |
||||
|
const currencies: { [symbol: string]: string } = {}; |
||||
|
|
||||
|
dates.push(resetHours(start)); |
||||
|
for (const item of this.transactionPoints[firstIndex - 1].items) { |
||||
|
dataGatheringItems.push({ |
||||
|
dataSource: item.dataSource, |
||||
|
symbol: item.symbol |
||||
|
}); |
||||
|
currencies[item.symbol] = item.currency; |
||||
|
} |
||||
|
for (let i = 0; i < this.transactionPoints.length; i++) { |
||||
|
if ( |
||||
|
!isBefore(parseDate(this.transactionPoints[i].date), start) && |
||||
|
firstTransactionPoint === null |
||||
|
) { |
||||
|
firstTransactionPoint = this.transactionPoints[i]; |
||||
|
firstIndex = i; |
||||
|
} |
||||
|
if (firstTransactionPoint !== null) { |
||||
|
dates.push(resetHours(parseDate(this.transactionPoints[i].date))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dates.push(resetHours(today)); |
||||
|
|
||||
|
const marketSymbols = await this.currentRateService.getValues({ |
||||
|
currencies, |
||||
|
dataGatheringItems, |
||||
|
dateQuery: { |
||||
|
in: dates |
||||
|
}, |
||||
|
userCurrency: this.currency |
||||
|
}); |
||||
|
|
||||
|
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 todayString = format(today, DATE_FORMAT); |
||||
|
|
||||
|
if (firstIndex > 0) { |
||||
|
firstIndex--; |
||||
|
} |
||||
|
const initialValues: { [symbol: string]: Big } = {}; |
||||
|
|
||||
|
const positions: TimelinePosition[] = []; |
||||
|
let hasErrorsInSymbolMetrics = false; |
||||
|
|
||||
|
for (const item of lastTransactionPoint.items) { |
||||
|
const marketValue = marketSymbolMap[todayString]?.[item.symbol]; |
||||
|
|
||||
|
const { |
||||
|
// annualizedGrossPerformance,
|
||||
|
// annualizedNetPerformance,
|
||||
|
grossPerformance, |
||||
|
grossPerformancePercentage, |
||||
|
hasErrors, |
||||
|
initialValue, |
||||
|
netPerformance, |
||||
|
netPerformancePercentage |
||||
|
} = this.getSymbolMetrics({ |
||||
|
marketSymbolMap, |
||||
|
start, |
||||
|
symbol: item.symbol |
||||
|
}); |
||||
|
|
||||
|
hasErrorsInSymbolMetrics = hasErrorsInSymbolMetrics || hasErrors; |
||||
|
|
||||
|
initialValues[item.symbol] = initialValue; |
||||
|
|
||||
|
positions.push({ |
||||
|
averagePrice: item.quantity.eq(0) |
||||
|
? new Big(0) |
||||
|
: item.investment.div(item.quantity), |
||||
|
currency: item.currency, |
||||
|
dataSource: item.dataSource, |
||||
|
firstBuyDate: item.firstBuyDate, |
||||
|
grossPerformance: !hasErrors ? grossPerformance ?? null : null, |
||||
|
grossPerformancePercentage: !hasErrors |
||||
|
? grossPerformancePercentage ?? null |
||||
|
: null, |
||||
|
investment: item.investment, |
||||
|
marketPrice: marketValue?.toNumber() ?? null, |
||||
|
netPerformance: !hasErrors ? netPerformance ?? null : null, |
||||
|
netPerformancePercentage: !hasErrors |
||||
|
? netPerformancePercentage ?? null |
||||
|
: null, |
||||
|
quantity: item.quantity, |
||||
|
symbol: item.symbol, |
||||
|
transactionCount: item.transactionCount |
||||
|
}); |
||||
|
} |
||||
|
const overall = this.calculateOverallPerformance(positions, initialValues); |
||||
|
|
||||
|
return { |
||||
|
...overall, |
||||
|
positions, |
||||
|
hasErrors: hasErrorsInSymbolMetrics || overall.hasErrors |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public getSymbolMetrics({ |
||||
|
marketSymbolMap, |
||||
|
start, |
||||
|
symbol |
||||
|
}: { |
||||
|
marketSymbolMap: { |
||||
|
[date: string]: { [symbol: string]: Big }; |
||||
|
}; |
||||
|
start: Date; |
||||
|
symbol: string; |
||||
|
}) { |
||||
|
let orders: PortfolioOrderItem[] = this.orders.filter((order) => { |
||||
|
return order.symbol === symbol; |
||||
|
}); |
||||
|
|
||||
|
if (orders.length <= 0) { |
||||
|
return { |
||||
|
hasErrors: false, |
||||
|
initialValue: new Big(0), |
||||
|
netPerformance: new Big(0), |
||||
|
netPerformancePercentage: new Big(0), |
||||
|
grossPerformance: new Big(0), |
||||
|
grossPerformancePercentage: new Big(0) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const dateOfFirstTransaction = new Date(first(orders).date); |
||||
|
const endDate = new Date(Date.now()); |
||||
|
|
||||
|
const unitPriceAtStartDate = |
||||
|
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; |
||||
|
|
||||
|
const unitPriceAtEndDate = |
||||
|
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol]; |
||||
|
|
||||
|
if ( |
||||
|
!unitPriceAtEndDate || |
||||
|
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) |
||||
|
) { |
||||
|
return { |
||||
|
hasErrors: true, |
||||
|
initialValue: new Big(0), |
||||
|
netPerformance: new Big(0), |
||||
|
netPerformancePercentage: new Big(0), |
||||
|
grossPerformance: new Big(0), |
||||
|
grossPerformancePercentage: new Big(0) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
let feesAtStartDate = new Big(0); |
||||
|
let fees = new Big(0); |
||||
|
let grossPerformance = new Big(0); |
||||
|
let grossPerformanceAtStartDate = new Big(0); |
||||
|
let grossPerformanceFromSells = new Big(0); |
||||
|
let initialValue: Big; |
||||
|
let lastAveragePrice = new Big(0); |
||||
|
let lastValueOfInvestment = new Big(0); |
||||
|
let lastNetValueOfInvestment = new Big(0); |
||||
|
let previousOrder: PortfolioOrder = null; |
||||
|
let timeWeightedGrossPerformancePercentage = new Big(1); |
||||
|
let timeWeightedNetPerformancePercentage = new Big(1); |
||||
|
let totalInvestment = new Big(0); |
||||
|
let totalUnits = new Big(0); |
||||
|
|
||||
|
// Add a synthetic order at the start and the end date
|
||||
|
orders.push({ |
||||
|
symbol, |
||||
|
currency: null, |
||||
|
date: format(start, DATE_FORMAT), |
||||
|
dataSource: null, |
||||
|
fee: new Big(0), |
||||
|
itemType: 'start', |
||||
|
name: '', |
||||
|
quantity: new Big(0), |
||||
|
type: TypeOfOrder.BUY, |
||||
|
unitPrice: unitPriceAtStartDate ?? new Big(0) |
||||
|
}); |
||||
|
|
||||
|
orders.push({ |
||||
|
symbol, |
||||
|
currency: null, |
||||
|
date: format(endDate, DATE_FORMAT), |
||||
|
dataSource: null, |
||||
|
fee: new Big(0), |
||||
|
itemType: 'end', |
||||
|
name: '', |
||||
|
quantity: new Big(0), |
||||
|
type: TypeOfOrder.BUY, |
||||
|
unitPrice: unitPriceAtEndDate ?? new Big(0) |
||||
|
}); |
||||
|
|
||||
|
// Sort orders so that the start and end placeholder order are at the right
|
||||
|
// position
|
||||
|
orders = sortBy(orders, (order) => { |
||||
|
let sortIndex = new Date(order.date); |
||||
|
|
||||
|
if (order.itemType === 'start') { |
||||
|
sortIndex = addMilliseconds(sortIndex, -1); |
||||
|
} |
||||
|
|
||||
|
if (order.itemType === 'end') { |
||||
|
sortIndex = addMilliseconds(sortIndex, 1); |
||||
|
} |
||||
|
|
||||
|
return sortIndex.getTime(); |
||||
|
}); |
||||
|
|
||||
|
const indexOfStartOrder = orders.findIndex((order) => { |
||||
|
return order.itemType === 'start'; |
||||
|
}); |
||||
|
|
||||
|
for (let i = 0; i < orders.length; i += 1) { |
||||
|
const order = orders[i]; |
||||
|
|
||||
|
const transactionInvestment = order.quantity.mul(order.unitPrice); |
||||
|
|
||||
|
if ( |
||||
|
!initialValue && |
||||
|
order.itemType !== 'start' && |
||||
|
order.itemType !== 'end' |
||||
|
) { |
||||
|
initialValue = transactionInvestment; |
||||
|
} |
||||
|
|
||||
|
fees = fees.plus(order.fee); |
||||
|
|
||||
|
totalUnits = totalUnits.plus( |
||||
|
order.quantity.mul(this.getFactor(order.type)) |
||||
|
); |
||||
|
|
||||
|
const valueOfInvestment = totalUnits.mul(order.unitPrice); |
||||
|
const netValueOfInvestment = totalUnits.mul(order.unitPrice).sub(fees); |
||||
|
|
||||
|
const grossPerformanceFromSell = |
||||
|
order.type === TypeOfOrder.SELL |
||||
|
? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) |
||||
|
: new Big(0); |
||||
|
|
||||
|
grossPerformanceFromSells = grossPerformanceFromSells.plus( |
||||
|
grossPerformanceFromSell |
||||
|
); |
||||
|
|
||||
|
totalInvestment = totalInvestment |
||||
|
.plus(transactionInvestment.mul(this.getFactor(order.type))) |
||||
|
.plus(grossPerformanceFromSell); |
||||
|
|
||||
|
lastAveragePrice = totalUnits.eq(0) |
||||
|
? new Big(0) |
||||
|
: totalInvestment.div(totalUnits); |
||||
|
|
||||
|
const newGrossPerformance = valueOfInvestment |
||||
|
.minus(totalInvestment) |
||||
|
.plus(grossPerformanceFromSells); |
||||
|
|
||||
|
const grossPerformanceSinceLastTransaction = |
||||
|
newGrossPerformance.minus(grossPerformance); |
||||
|
|
||||
|
const netPerformanceSinceLastTransaction = |
||||
|
grossPerformanceSinceLastTransaction.minus(previousOrder?.fee ?? 0); |
||||
|
|
||||
|
if ( |
||||
|
i > indexOfStartOrder && |
||||
|
!lastValueOfInvestment |
||||
|
.plus(transactionInvestment.mul(this.getFactor(order.type))) |
||||
|
.eq(0) |
||||
|
) { |
||||
|
timeWeightedGrossPerformancePercentage = |
||||
|
timeWeightedGrossPerformancePercentage.mul( |
||||
|
new Big(1).plus( |
||||
|
valueOfInvestment |
||||
|
.minus( |
||||
|
lastValueOfInvestment.plus( |
||||
|
transactionInvestment.mul(this.getFactor(order.type)) |
||||
|
) |
||||
|
) |
||||
|
.div( |
||||
|
lastValueOfInvestment.plus( |
||||
|
transactionInvestment.mul(this.getFactor(order.type)) |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
timeWeightedNetPerformancePercentage = |
||||
|
timeWeightedNetPerformancePercentage.mul( |
||||
|
new Big(1).plus( |
||||
|
netValueOfInvestment |
||||
|
.minus( |
||||
|
lastNetValueOfInvestment.plus( |
||||
|
transactionInvestment.mul(this.getFactor(order.type)) |
||||
|
) |
||||
|
) |
||||
|
.div( |
||||
|
lastNetValueOfInvestment.plus( |
||||
|
transactionInvestment.mul(this.getFactor(order.type)) |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
grossPerformance = newGrossPerformance; |
||||
|
lastNetValueOfInvestment = netValueOfInvestment; |
||||
|
lastValueOfInvestment = valueOfInvestment; |
||||
|
|
||||
|
if (order.itemType === 'start') { |
||||
|
feesAtStartDate = fees; |
||||
|
grossPerformanceAtStartDate = grossPerformance; |
||||
|
} |
||||
|
|
||||
|
/*console.log(` |
||||
|
Symbol: ${symbol} |
||||
|
Date: ${order.date} |
||||
|
Price: ${order.unitPrice} |
||||
|
transactionInvestment: ${transactionInvestment} |
||||
|
totalUnits: ${totalUnits} |
||||
|
totalInvestment: ${totalInvestment} |
||||
|
valueOfInvestment: ${valueOfInvestment} |
||||
|
lastAveragePrice: ${lastAveragePrice} |
||||
|
grossPerformanceFromSell: ${grossPerformanceFromSell} |
||||
|
grossPerformanceFromSells: ${grossPerformanceFromSells} |
||||
|
grossPerformance: ${grossPerformance.minus(grossPerformanceAtStartDate)} |
||||
|
netPerformance: ${grossPerformance.minus(fees)} |
||||
|
netPerformanceSinceLastTransaction: ${netPerformanceSinceLastTransaction} |
||||
|
grossPerformanceSinceLastTransaction: ${grossPerformanceSinceLastTransaction} |
||||
|
timeWeightedGrossPerformancePercentage: ${timeWeightedGrossPerformancePercentage} |
||||
|
timeWeightedNetPerformancePercentage: ${timeWeightedNetPerformancePercentage} |
||||
|
`);*/
|
||||
|
|
||||
|
previousOrder = order; |
||||
|
} |
||||
|
|
||||
|
// console.log('\n---\n');
|
||||
|
|
||||
|
timeWeightedGrossPerformancePercentage = |
||||
|
timeWeightedGrossPerformancePercentage.sub(1); |
||||
|
|
||||
|
timeWeightedNetPerformancePercentage = |
||||
|
timeWeightedNetPerformancePercentage.sub(1); |
||||
|
|
||||
|
const totalGrossPerformance = grossPerformance.minus( |
||||
|
grossPerformanceAtStartDate |
||||
|
); |
||||
|
|
||||
|
const totalNetPerformance = grossPerformance |
||||
|
.minus(grossPerformanceAtStartDate) |
||||
|
.minus(fees.minus(feesAtStartDate)); |
||||
|
|
||||
|
return { |
||||
|
hasErrors: !initialValue || !unitPriceAtEndDate, |
||||
|
initialValue, |
||||
|
netPerformance: totalNetPerformance, |
||||
|
netPerformancePercentage: timeWeightedNetPerformancePercentage, |
||||
|
grossPerformance: totalGrossPerformance, |
||||
|
grossPerformancePercentage: timeWeightedGrossPerformancePercentage |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
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.add(transactionPointSymbol.investment), |
||||
|
new Big(0) |
||||
|
) |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async calculateTimeline( |
||||
|
timelineSpecification: TimelineSpecification[], |
||||
|
endDate: string |
||||
|
): Promise<TimelineInfoInterface> { |
||||
|
if (timelineSpecification.length === 0) { |
||||
|
return { |
||||
|
maxNetPerformance: new Big(0), |
||||
|
minNetPerformance: new Big(0), |
||||
|
timelinePeriods: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const startDate = timelineSpecification[0].start; |
||||
|
const start = parseDate(startDate); |
||||
|
const end = parseDate(endDate); |
||||
|
|
||||
|
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = []; |
||||
|
let i = 0; |
||||
|
let j = -1; |
||||
|
for ( |
||||
|
let currentDate = start; |
||||
|
!isAfter(currentDate, end); |
||||
|
currentDate = this.addToDate( |
||||
|
currentDate, |
||||
|
timelineSpecification[i].accuracy |
||||
|
) |
||||
|
) { |
||||
|
if (this.isNextItemActive(timelineSpecification, currentDate, i)) { |
||||
|
i++; |
||||
|
} |
||||
|
while ( |
||||
|
j + 1 < this.transactionPoints.length && |
||||
|
!isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate) |
||||
|
) { |
||||
|
j++; |
||||
|
} |
||||
|
|
||||
|
let periodEndDate = currentDate; |
||||
|
if (timelineSpecification[i].accuracy === 'day') { |
||||
|
let nextEndDate = end; |
||||
|
if (j + 1 < this.transactionPoints.length) { |
||||
|
nextEndDate = parseDate(this.transactionPoints[j + 1].date); |
||||
|
} |
||||
|
periodEndDate = min([ |
||||
|
addMonths(currentDate, 3), |
||||
|
max([currentDate, nextEndDate]) |
||||
|
]); |
||||
|
} |
||||
|
const timePeriodForDates = this.getTimePeriodForDate( |
||||
|
j, |
||||
|
currentDate, |
||||
|
endOfDay(periodEndDate) |
||||
|
); |
||||
|
currentDate = periodEndDate; |
||||
|
if (timePeriodForDates != null) { |
||||
|
timelinePeriodPromises.push(timePeriodForDates); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all( |
||||
|
timelinePeriodPromises |
||||
|
); |
||||
|
const minNetPerformance = timelineInfoInterfaces |
||||
|
.map((timelineInfo) => timelineInfo.minNetPerformance) |
||||
|
.filter((performance) => performance !== null) |
||||
|
.reduce((minPerformance, current) => { |
||||
|
if (minPerformance.lt(current)) { |
||||
|
return minPerformance; |
||||
|
} else { |
||||
|
return current; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const maxNetPerformance = timelineInfoInterfaces |
||||
|
.map((timelineInfo) => timelineInfo.maxNetPerformance) |
||||
|
.filter((performance) => performance !== null) |
||||
|
.reduce((maxPerformance, current) => { |
||||
|
if (maxPerformance.gt(current)) { |
||||
|
return maxPerformance; |
||||
|
} else { |
||||
|
return current; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const timelinePeriods = timelineInfoInterfaces.map( |
||||
|
(timelineInfo) => timelineInfo.timelinePeriods |
||||
|
); |
||||
|
|
||||
|
return { |
||||
|
maxNetPerformance, |
||||
|
minNetPerformance, |
||||
|
timelinePeriods: flatten(timelinePeriods) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private calculateOverallPerformance( |
||||
|
positions: TimelinePosition[], |
||||
|
initialValues: { [p: string]: Big } |
||||
|
) { |
||||
|
let hasErrors = false; |
||||
|
let currentValue = new Big(0); |
||||
|
let totalInvestment = new Big(0); |
||||
|
let grossPerformance = new Big(0); |
||||
|
let grossPerformancePercentage = new Big(0); |
||||
|
let netPerformance = new Big(0); |
||||
|
let netPerformancePercentage = new Big(0); |
||||
|
let completeInitialValue = new Big(0); |
||||
|
let netAnnualizedPerformance = new Big(0); |
||||
|
|
||||
|
// use Date.now() to use the mock for today
|
||||
|
const today = new Date(Date.now()); |
||||
|
|
||||
|
for (const currentPosition of positions) { |
||||
|
if (currentPosition.marketPrice) { |
||||
|
currentValue = currentValue.add( |
||||
|
new Big(currentPosition.marketPrice).mul(currentPosition.quantity) |
||||
|
); |
||||
|
} else { |
||||
|
hasErrors = true; |
||||
|
} |
||||
|
totalInvestment = totalInvestment.add(currentPosition.investment); |
||||
|
if (currentPosition.grossPerformance) { |
||||
|
grossPerformance = grossPerformance.plus( |
||||
|
currentPosition.grossPerformance |
||||
|
); |
||||
|
netPerformance = netPerformance.plus(currentPosition.netPerformance); |
||||
|
} else if (!currentPosition.quantity.eq(0)) { |
||||
|
hasErrors = true; |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
currentPosition.grossPerformancePercentage && |
||||
|
initialValues[currentPosition.symbol] |
||||
|
) { |
||||
|
const currentInitialValue = initialValues[currentPosition.symbol]; |
||||
|
completeInitialValue = completeInitialValue.plus(currentInitialValue); |
||||
|
grossPerformancePercentage = grossPerformancePercentage.plus( |
||||
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue) |
||||
|
); |
||||
|
|
||||
|
netAnnualizedPerformance = netAnnualizedPerformance.plus( |
||||
|
this.getAnnualizedPerformancePercent({ |
||||
|
daysInMarket: differenceInDays( |
||||
|
today, |
||||
|
parseDate(currentPosition.firstBuyDate) |
||||
|
), |
||||
|
netPerformancePercent: currentPosition.netPerformancePercentage |
||||
|
}).mul(currentInitialValue) |
||||
|
); |
||||
|
netPerformancePercentage = netPerformancePercentage.plus( |
||||
|
currentPosition.netPerformancePercentage.mul(currentInitialValue) |
||||
|
); |
||||
|
} else if (!currentPosition.quantity.eq(0)) { |
||||
|
Logger.warn( |
||||
|
`Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}` |
||||
|
); |
||||
|
hasErrors = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!completeInitialValue.eq(0)) { |
||||
|
grossPerformancePercentage = |
||||
|
grossPerformancePercentage.div(completeInitialValue); |
||||
|
netPerformancePercentage = |
||||
|
netPerformancePercentage.div(completeInitialValue); |
||||
|
netAnnualizedPerformance = |
||||
|
netAnnualizedPerformance.div(completeInitialValue); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
currentValue, |
||||
|
grossPerformance, |
||||
|
grossPerformancePercentage, |
||||
|
hasErrors, |
||||
|
netAnnualizedPerformance, |
||||
|
netPerformance, |
||||
|
netPerformancePercentage, |
||||
|
totalInvestment |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private async getTimePeriodForDate( |
||||
|
j: number, |
||||
|
startDate: Date, |
||||
|
endDate: Date |
||||
|
): Promise<TimelineInfoInterface> { |
||||
|
let investment: Big = new Big(0); |
||||
|
let fees: Big = new Big(0); |
||||
|
|
||||
|
const marketSymbolMap: { |
||||
|
[date: string]: { [symbol: string]: Big }; |
||||
|
} = {}; |
||||
|
if (j >= 0) { |
||||
|
const currencies: { [name: string]: string } = {}; |
||||
|
const dataGatheringItems: IDataGatheringItem[] = []; |
||||
|
|
||||
|
for (const item of this.transactionPoints[j].items) { |
||||
|
currencies[item.symbol] = item.currency; |
||||
|
dataGatheringItems.push({ |
||||
|
dataSource: item.dataSource, |
||||
|
symbol: item.symbol |
||||
|
}); |
||||
|
investment = investment.add(item.investment); |
||||
|
fees = fees.add(item.fee); |
||||
|
} |
||||
|
|
||||
|
let marketSymbols: GetValueObject[] = []; |
||||
|
if (dataGatheringItems.length > 0) { |
||||
|
try { |
||||
|
marketSymbols = await this.currentRateService.getValues({ |
||||
|
currencies, |
||||
|
dataGatheringItems, |
||||
|
dateQuery: { |
||||
|
gte: startDate, |
||||
|
lt: endOfDay(endDate) |
||||
|
}, |
||||
|
userCurrency: this.currency |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
Logger.error( |
||||
|
`Failed to fetch info for date ${startDate} with exception`, |
||||
|
error |
||||
|
); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 results: TimelinePeriod[] = []; |
||||
|
let maxNetPerformance: Big = null; |
||||
|
let minNetPerformance: Big = null; |
||||
|
for ( |
||||
|
let currentDate = startDate; |
||||
|
isBefore(currentDate, endDate); |
||||
|
currentDate = addDays(currentDate, 1) |
||||
|
) { |
||||
|
let value = new Big(0); |
||||
|
const currentDateAsString = format(currentDate, DATE_FORMAT); |
||||
|
let invalid = false; |
||||
|
if (j >= 0) { |
||||
|
for (const item of this.transactionPoints[j].items) { |
||||
|
if ( |
||||
|
!marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol) |
||||
|
) { |
||||
|
invalid = true; |
||||
|
break; |
||||
|
} |
||||
|
value = value.add( |
||||
|
item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol]) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
if (!invalid) { |
||||
|
const grossPerformance = value.minus(investment); |
||||
|
const netPerformance = grossPerformance.minus(fees); |
||||
|
if ( |
||||
|
minNetPerformance === null || |
||||
|
minNetPerformance.gt(netPerformance) |
||||
|
) { |
||||
|
minNetPerformance = netPerformance; |
||||
|
} |
||||
|
if ( |
||||
|
maxNetPerformance === null || |
||||
|
maxNetPerformance.lt(netPerformance) |
||||
|
) { |
||||
|
maxNetPerformance = netPerformance; |
||||
|
} |
||||
|
|
||||
|
const result = { |
||||
|
grossPerformance, |
||||
|
investment, |
||||
|
netPerformance, |
||||
|
value, |
||||
|
date: currentDateAsString |
||||
|
}; |
||||
|
results.push(result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
maxNetPerformance, |
||||
|
minNetPerformance, |
||||
|
timelinePeriods: results |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private getFactor(type: TypeOfOrder) { |
||||
|
let factor: number; |
||||
|
|
||||
|
switch (type) { |
||||
|
case 'BUY': |
||||
|
factor = 1; |
||||
|
break; |
||||
|
case 'SELL': |
||||
|
factor = -1; |
||||
|
break; |
||||
|
default: |
||||
|
factor = 0; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
return factor; |
||||
|
} |
||||
|
|
||||
|
private addToDate(date: Date, accuracy: Accuracy): Date { |
||||
|
switch (accuracy) { |
||||
|
case 'day': |
||||
|
return addDays(date, 1); |
||||
|
case 'month': |
||||
|
return addMonths(date, 1); |
||||
|
case 'year': |
||||
|
return addYears(date, 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private isNextItemActive( |
||||
|
timelineSpecification: TimelineSpecification[], |
||||
|
currentDate: Date, |
||||
|
i: number |
||||
|
) { |
||||
|
return ( |
||||
|
i + 1 < timelineSpecification.length && |
||||
|
!isBefore(currentDate, parseDate(timelineSpecification[i + 1].start)) |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { PortfolioService } from './portfolio.service'; |
||||
|
import { PortfolioServiceNew } from './portfolio.service-new'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class PortfolioServiceFactory { |
||||
|
public constructor( |
||||
|
private readonly portfolioService: PortfolioService, |
||||
|
private readonly portfolioServiceNew: PortfolioServiceNew |
||||
|
) {} |
||||
|
|
||||
|
public get() { |
||||
|
if (false) { |
||||
|
return this.portfolioServiceNew; |
||||
|
} |
||||
|
|
||||
|
return this.portfolioService; |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
Loading…
Reference in new issue