|
|
@ -26,11 +26,15 @@ import { |
|
|
|
getYear, |
|
|
|
isAfter, |
|
|
|
isSameDay, |
|
|
|
max, |
|
|
|
parse, |
|
|
|
parseISO, |
|
|
|
setDate, |
|
|
|
setDayOfYear, |
|
|
|
setMonth, |
|
|
|
sub |
|
|
|
sub, |
|
|
|
subDays, |
|
|
|
subYears |
|
|
|
} from 'date-fns'; |
|
|
|
import { isEmpty } from 'lodash'; |
|
|
|
import * as roundTo from 'round-to'; |
|
|
@ -39,6 +43,14 @@ import { |
|
|
|
HistoricalDataItem, |
|
|
|
PortfolioPositionDetail |
|
|
|
} from './interfaces/portfolio-position-detail.interface'; |
|
|
|
import { |
|
|
|
PortfolioCalculator, |
|
|
|
PortfolioOrder, |
|
|
|
TimelineSpecification |
|
|
|
} from '@ghostfolio/api/app/core/portfolio-calculator'; |
|
|
|
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; |
|
|
|
import Big from 'big.js'; |
|
|
|
import { port } from 'envalid'; |
|
|
|
|
|
|
|
@Injectable() |
|
|
|
export class PortfolioService { |
|
|
@ -51,7 +63,8 @@ export class PortfolioService { |
|
|
|
private readonly redisCacheService: RedisCacheService, |
|
|
|
@Inject(REQUEST) private readonly request: RequestWithUser, |
|
|
|
private readonly rulesService: RulesService, |
|
|
|
private readonly userService: UserService |
|
|
|
private readonly userService: UserService, |
|
|
|
private readonly currentRateService: CurrentRateService |
|
|
|
) {} |
|
|
|
|
|
|
|
public async createPortfolio(aUserId: string): Promise<Portfolio> { |
|
|
@ -148,7 +161,8 @@ export class PortfolioService { |
|
|
|
impersonationUserId || this.request.user.id |
|
|
|
); |
|
|
|
|
|
|
|
if (portfolio.getOrders().length <= 0) { |
|
|
|
const orders = portfolio.getOrders(); |
|
|
|
if (orders.length <= 0) { |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
@ -157,10 +171,14 @@ export class PortfolioService { |
|
|
|
portfolio.getMinDate() |
|
|
|
); |
|
|
|
|
|
|
|
return portfolio |
|
|
|
.get() |
|
|
|
const portfolioCalculator = new PortfolioCalculator( |
|
|
|
this.currentRateService, |
|
|
|
this.request.user.Settings.currency |
|
|
|
); |
|
|
|
|
|
|
|
const portfolioOrders: PortfolioOrder[] = orders |
|
|
|
.filter((portfolioItem) => { |
|
|
|
if (isAfter(parseISO(portfolioItem.date), endOfToday())) { |
|
|
|
if (isAfter(parseISO(portfolioItem.getDate()), endOfToday())) { |
|
|
|
// Filter out future dates
|
|
|
|
return false; |
|
|
|
} |
|
|
@ -170,18 +188,70 @@ export class PortfolioService { |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
isSameDay(parseISO(portfolioItem.date), dateRangeDate) || |
|
|
|
isAfter(parseISO(portfolioItem.date), dateRangeDate) |
|
|
|
isSameDay(parseISO(portfolioItem.getDate()), dateRangeDate) || |
|
|
|
isAfter(parseISO(portfolioItem.getDate()), dateRangeDate) |
|
|
|
); |
|
|
|
}) |
|
|
|
.map((portfolioItem) => { |
|
|
|
return { |
|
|
|
date: format(parseISO(portfolioItem.date), 'yyyy-MM-dd'), |
|
|
|
grossPerformancePercent: portfolioItem.grossPerformancePercent, |
|
|
|
marketPrice: portfolioItem.value ?? null, |
|
|
|
value: portfolioItem.value - portfolioItem.investment ?? null |
|
|
|
}; |
|
|
|
}); |
|
|
|
.map((order) => ({ |
|
|
|
date: order.getDate().substr(0, 10), |
|
|
|
quantity: new Big(order.getQuantity()), |
|
|
|
symbol: order.getSymbol(), |
|
|
|
type: order.getType(), |
|
|
|
unitPrice: new Big(order.getUnitPrice()), |
|
|
|
currency: order.getCurrency() |
|
|
|
})); |
|
|
|
portfolioCalculator.computeTransactionPoints(portfolioOrders); |
|
|
|
const transactionPoints = portfolioCalculator.getTransactionPoints(); |
|
|
|
if (transactionPoints.length === 0) { |
|
|
|
return []; |
|
|
|
} |
|
|
|
const dateFormat = 'yyyy-MM-dd'; |
|
|
|
let portfolioStart = parse( |
|
|
|
transactionPoints[0].date, |
|
|
|
dateFormat, |
|
|
|
new Date() |
|
|
|
); |
|
|
|
portfolioStart = this.getStartDate(aDateRange, portfolioStart); |
|
|
|
|
|
|
|
const timelineSpecification: TimelineSpecification[] = [ |
|
|
|
{ |
|
|
|
start: format(portfolioStart, dateFormat), |
|
|
|
accuracy: 'month' |
|
|
|
}, |
|
|
|
{ |
|
|
|
start: format(subYears(new Date(), 1), dateFormat), |
|
|
|
accuracy: 'day' |
|
|
|
} |
|
|
|
]; |
|
|
|
|
|
|
|
const timeline = await portfolioCalculator.calculateTimeline( |
|
|
|
timelineSpecification, |
|
|
|
format(new Date(), dateFormat) |
|
|
|
); |
|
|
|
|
|
|
|
return timeline.map((timelineItem) => ({ |
|
|
|
date: timelineItem.date, |
|
|
|
value: timelineItem.grossPerformance, |
|
|
|
marketPrice: timelineItem.value |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
private getStartDate(aDateRange: DateRange, portfolioStart: Date) { |
|
|
|
switch (aDateRange) { |
|
|
|
case '1d': |
|
|
|
portfolioStart = max([portfolioStart, subDays(new Date(), 1)]); |
|
|
|
break; |
|
|
|
case 'ytd': |
|
|
|
portfolioStart = max([portfolioStart, setDayOfYear(new Date(), 1)]); |
|
|
|
break; |
|
|
|
case '1y': |
|
|
|
portfolioStart = max([portfolioStart, subYears(new Date(), 1)]); |
|
|
|
break; |
|
|
|
case '5y': |
|
|
|
portfolioStart = max([portfolioStart, subYears(new Date(), 5)]); |
|
|
|
break; |
|
|
|
} |
|
|
|
return portfolioStart; |
|
|
|
} |
|
|
|
|
|
|
|
public async getOverview( |
|
|
|