|
|
@ -1,4 +1,3 @@ |
|
|
|
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; |
|
|
|
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'; |
|
|
@ -20,32 +19,19 @@ import { |
|
|
|
differenceInDays, |
|
|
|
endOfDay, |
|
|
|
format, |
|
|
|
isAfter, |
|
|
|
isBefore, |
|
|
|
isSameDay, |
|
|
|
isSameMonth, |
|
|
|
isSameYear, |
|
|
|
max, |
|
|
|
min, |
|
|
|
set, |
|
|
|
subDays |
|
|
|
} from 'date-fns'; |
|
|
|
import { |
|
|
|
cloneDeep, |
|
|
|
first, |
|
|
|
flatten, |
|
|
|
isNumber, |
|
|
|
last, |
|
|
|
sortBy, |
|
|
|
uniq |
|
|
|
} from 'lodash'; |
|
|
|
import { cloneDeep, first, isNumber, last, sortBy, uniq } 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 |
|
|
@ -776,107 +762,6 @@ export class PortfolioCalculator { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let minNetPerformance = new Big(0); |
|
|
|
let maxNetPerformance = new Big(0); |
|
|
|
|
|
|
|
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all( |
|
|
|
timelinePeriodPromises |
|
|
|
); |
|
|
|
|
|
|
|
try { |
|
|
|
minNetPerformance = timelineInfoInterfaces |
|
|
|
.map((timelineInfo) => timelineInfo.minNetPerformance) |
|
|
|
.filter((performance) => performance !== null) |
|
|
|
.reduce((minPerformance, current) => { |
|
|
|
if (minPerformance.lt(current)) { |
|
|
|
return minPerformance; |
|
|
|
} else { |
|
|
|
return current; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
maxNetPerformance = timelineInfoInterfaces |
|
|
|
.map((timelineInfo) => timelineInfo.maxNetPerformance) |
|
|
|
.filter((performance) => performance !== null) |
|
|
|
.reduce((maxPerformance, current) => { |
|
|
|
if (maxPerformance.gt(current)) { |
|
|
|
return maxPerformance; |
|
|
|
} else { |
|
|
|
return current; |
|
|
|
} |
|
|
|
}); |
|
|
|
} catch {} |
|
|
|
|
|
|
|
const timelinePeriods = timelineInfoInterfaces.map( |
|
|
|
(timelineInfo) => timelineInfo.timelinePeriods |
|
|
|
); |
|
|
|
|
|
|
|
return { |
|
|
|
maxNetPerformance, |
|
|
|
minNetPerformance, |
|
|
|
timelinePeriods: flatten(timelinePeriods) |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private calculateOverallPerformance(positions: TimelinePosition[]) { |
|
|
|
let currentValue = new Big(0); |
|
|
|
let grossPerformance = new Big(0); |
|
|
@ -983,123 +868,6 @@ export class PortfolioCalculator { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
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.plus(item.investment); |
|
|
|
fees = fees.plus(item.fee); |
|
|
|
} |
|
|
|
|
|
|
|
let marketSymbols: GetValueObject[] = []; |
|
|
|
if (dataGatheringItems.length > 0) { |
|
|
|
try { |
|
|
|
const { values } = await this.currentRateService.getValues({ |
|
|
|
dataGatheringItems, |
|
|
|
dateQuery: { |
|
|
|
gte: startDate, |
|
|
|
lt: endOfDay(endDate) |
|
|
|
} |
|
|
|
}); |
|
|
|
marketSymbols = values; |
|
|
|
} catch (error) { |
|
|
|
Logger.error( |
|
|
|
`Failed to fetch info for date ${startDate} with exception`, |
|
|
|
error, |
|
|
|
'PortfolioCalculator' |
|
|
|
); |
|
|
|
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.plus( |
|
|
|
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; |
|
|
|
|
|
|
|