|
|
@ -3,7 +3,7 @@ import { |
|
|
|
GetValueObject |
|
|
|
} from '@ghostfolio/api/app/core/current-rate.service'; |
|
|
|
import { OrderType } from '@ghostfolio/api/models/order-type'; |
|
|
|
import { resetHours } from '@ghostfolio/common/helper'; |
|
|
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
|
|
|
import { TimelinePosition } from '@ghostfolio/common/interfaces'; |
|
|
|
import { Currency } from '@prisma/client'; |
|
|
|
import Big from 'big.js'; |
|
|
@ -17,17 +17,10 @@ import { |
|
|
|
isBefore, |
|
|
|
max, |
|
|
|
min, |
|
|
|
parse, |
|
|
|
subDays |
|
|
|
} from 'date-fns'; |
|
|
|
import { flatten } from 'lodash'; |
|
|
|
|
|
|
|
const DATE_FORMAT = 'yyyy-MM-dd'; |
|
|
|
|
|
|
|
function dparse(date: string) { |
|
|
|
return parse(date, DATE_FORMAT, new Date()); |
|
|
|
} |
|
|
|
|
|
|
|
export class PortfolioCalculator { |
|
|
|
private transactionPoints: TransactionPoint[]; |
|
|
|
|
|
|
@ -115,7 +108,7 @@ export class PortfolioCalculator { |
|
|
|
return this.transactionPoints; |
|
|
|
} |
|
|
|
|
|
|
|
public async getCurrentPositions(): Promise<{ |
|
|
|
public async getCurrentPositions(start: Date): Promise<{ |
|
|
|
[symbol: string]: TimelinePosition; |
|
|
|
}> { |
|
|
|
if (!this.transactionPoints?.length) { |
|
|
@ -126,29 +119,117 @@ export class PortfolioCalculator { |
|
|
|
this.transactionPoints[this.transactionPoints.length - 1]; |
|
|
|
|
|
|
|
const result: { [symbol: string]: TimelinePosition } = {}; |
|
|
|
const marketValues = await this.getMarketValues( |
|
|
|
lastTransactionPoint, |
|
|
|
resetHours(subDays(new Date(), 3)), |
|
|
|
endOfDay(new Date()) |
|
|
|
); |
|
|
|
// use Date.now() to use the mock for today
|
|
|
|
const today = new Date(Date.now()); |
|
|
|
|
|
|
|
for (const item of lastTransactionPoint.items) { |
|
|
|
const marketValue = marketValues[item.symbol]; |
|
|
|
const grossPerformance = marketValue |
|
|
|
? new Big(marketValue.marketPrice) |
|
|
|
let firstTransactionPoint: TransactionPoint = null; |
|
|
|
let firstIndex = this.transactionPoints.length; |
|
|
|
const dates = []; |
|
|
|
const symbols = new Set<string>(); |
|
|
|
const currencies: { [symbol: string]: Currency } = {}; |
|
|
|
|
|
|
|
dates.push(resetHours(start)); |
|
|
|
for (const item of this.transactionPoints[firstIndex - 1].items) { |
|
|
|
symbols.add(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))); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const yesterday = resetHours(subDays(today, 1)); |
|
|
|
if (dates.indexOf(yesterday) === -1) { |
|
|
|
dates.push(yesterday); |
|
|
|
} |
|
|
|
dates.push(resetHours(today)); |
|
|
|
|
|
|
|
const marketSymbols = await this.currentRateService.getValues({ |
|
|
|
currencies, |
|
|
|
dateQuery: { |
|
|
|
in: dates |
|
|
|
}, |
|
|
|
symbols: Array.from(symbols), |
|
|
|
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] = {}; |
|
|
|
} |
|
|
|
marketSymbolMap[date][marketSymbol.symbol] = new Big( |
|
|
|
marketSymbol.marketPrice |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const startString = format(start, DATE_FORMAT); |
|
|
|
|
|
|
|
const holdingPeriodReturns: { [symbol: string]: Big } = {}; |
|
|
|
const grossPerformance: { [symbol: string]: Big } = {}; |
|
|
|
let todayString = format(today, DATE_FORMAT); |
|
|
|
// in case no symbols are there for today, use yesterday
|
|
|
|
if (!marketSymbolMap[todayString]) { |
|
|
|
todayString = format(subDays(today, 1), DATE_FORMAT); |
|
|
|
} |
|
|
|
|
|
|
|
if (firstIndex > 0) { |
|
|
|
firstIndex--; |
|
|
|
} |
|
|
|
for (let i = firstIndex; i < this.transactionPoints.length; i++) { |
|
|
|
const currentDate = |
|
|
|
i === firstIndex ? startString : this.transactionPoints[i].date; |
|
|
|
const nextDate = |
|
|
|
i + 1 < this.transactionPoints.length |
|
|
|
? this.transactionPoints[i + 1].date |
|
|
|
: todayString; |
|
|
|
|
|
|
|
const items = this.transactionPoints[i].items; |
|
|
|
for (const item of items) { |
|
|
|
let oldHoldingPeriodReturn = holdingPeriodReturns[item.symbol]; |
|
|
|
if (!oldHoldingPeriodReturn) { |
|
|
|
oldHoldingPeriodReturn = new Big(1); |
|
|
|
} |
|
|
|
holdingPeriodReturns[item.symbol] = oldHoldingPeriodReturn.mul( |
|
|
|
marketSymbolMap[nextDate][item.symbol].div( |
|
|
|
marketSymbolMap[currentDate][item.symbol] |
|
|
|
) |
|
|
|
); |
|
|
|
let oldGrossPerformance = grossPerformance[item.symbol]; |
|
|
|
if (!oldGrossPerformance) { |
|
|
|
oldGrossPerformance = new Big(0); |
|
|
|
} |
|
|
|
grossPerformance[item.symbol] = oldGrossPerformance.plus( |
|
|
|
marketSymbolMap[nextDate][item.symbol] |
|
|
|
.minus(marketSymbolMap[currentDate][item.symbol]) |
|
|
|
.mul(item.quantity) |
|
|
|
.minus(item.investment) |
|
|
|
: null; |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
for (const item of lastTransactionPoint.items) { |
|
|
|
const marketValue = marketSymbolMap[todayString][item.symbol]; |
|
|
|
result[item.symbol] = { |
|
|
|
averagePrice: item.investment.div(item.quantity), |
|
|
|
currency: item.currency, |
|
|
|
firstBuyDate: item.firstBuyDate, |
|
|
|
grossPerformance, |
|
|
|
grossPerformancePercentage: marketValue |
|
|
|
? grossPerformance.div(item.investment) |
|
|
|
grossPerformance: grossPerformance[item.symbol] ?? null, |
|
|
|
grossPerformancePercentage: holdingPeriodReturns[item.symbol] |
|
|
|
? holdingPeriodReturns[item.symbol].minus(1) |
|
|
|
: null, |
|
|
|
investment: item.investment, |
|
|
|
marketPrice: marketValue?.marketPrice, |
|
|
|
marketPrice: marketValue.toNumber(), |
|
|
|
name: item.name, |
|
|
|
quantity: item.quantity, |
|
|
|
symbol: item.symbol, |
|
|
@ -170,8 +251,8 @@ export class PortfolioCalculator { |
|
|
|
console.time('calculate-timeline-calculations'); |
|
|
|
|
|
|
|
const startDate = timelineSpecification[0].start; |
|
|
|
const start = dparse(startDate); |
|
|
|
const end = dparse(endDate); |
|
|
|
const start = parseDate(startDate); |
|
|
|
const end = parseDate(endDate); |
|
|
|
|
|
|
|
const timelinePeriodPromises: Promise<TimelinePeriod[]>[] = []; |
|
|
|
let i = 0; |
|
|
@ -189,7 +270,7 @@ export class PortfolioCalculator { |
|
|
|
} |
|
|
|
while ( |
|
|
|
j + 1 < this.transactionPoints.length && |
|
|
|
!isAfter(dparse(this.transactionPoints[j + 1].date), currentDate) |
|
|
|
!isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate) |
|
|
|
) { |
|
|
|
j++; |
|
|
|
} |
|
|
@ -198,7 +279,7 @@ export class PortfolioCalculator { |
|
|
|
if (timelineSpecification[i].accuracy === 'day') { |
|
|
|
let nextEndDate = end; |
|
|
|
if (j + 1 < this.transactionPoints.length) { |
|
|
|
nextEndDate = dparse(this.transactionPoints[j + 1].date); |
|
|
|
nextEndDate = parseDate(this.transactionPoints[j + 1].date); |
|
|
|
} |
|
|
|
periodEndDate = min([ |
|
|
|
addMonths(currentDate, 3), |
|
|
@ -242,8 +323,10 @@ export class PortfolioCalculator { |
|
|
|
currencies[item.symbol] = item.currency; |
|
|
|
} |
|
|
|
const values = await this.currentRateService.getValues({ |
|
|
|
dateRangeStart, |
|
|
|
dateRangeEnd, |
|
|
|
dateQuery: { |
|
|
|
gte: dateRangeStart, |
|
|
|
lt: endOfDay(dateRangeEnd) |
|
|
|
}, |
|
|
|
symbols, |
|
|
|
currencies, |
|
|
|
userCurrency: this.currency |
|
|
@ -280,8 +363,10 @@ export class PortfolioCalculator { |
|
|
|
if (symbols.length > 0) { |
|
|
|
try { |
|
|
|
marketSymbols = await this.currentRateService.getValues({ |
|
|
|
dateRangeStart: startDate, |
|
|
|
dateRangeEnd: endDate, |
|
|
|
dateQuery: { |
|
|
|
gte: startDate, |
|
|
|
lt: endOfDay(endDate) |
|
|
|
}, |
|
|
|
symbols, |
|
|
|
currencies, |
|
|
|
userCurrency: this.currency |
|
|
@ -376,7 +461,7 @@ export class PortfolioCalculator { |
|
|
|
) { |
|
|
|
return ( |
|
|
|
i + 1 < timelineSpecification.length && |
|
|
|
!isBefore(currentDate, dparse(timelineSpecification[i + 1].start)) |
|
|
|
!isBefore(currentDate, parseDate(timelineSpecification[i + 1].start)) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|