diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 84a7f1db5..e335ab22b 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -18,6 +18,8 @@ import { Module } from '@nestjs/common'; import { PortfolioController } from './portfolio.controller'; import { PortfolioService } from './portfolio.service'; +import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; +import { MarketDataService } from '@ghostfolio/api/app/core/market-data.service'; @Module({ imports: [RedisCacheModule], @@ -26,6 +28,7 @@ import { PortfolioService } from './portfolio.service'; AccountService, AlphaVantageService, CacheService, + CurrentRateService, ConfigurationService, DataGatheringService, DataProviderService, @@ -37,6 +40,7 @@ import { PortfolioService } from './portfolio.service'; PrismaService, RakutenRapidApiService, RulesService, + MarketDataService, UserService, YahooFinanceService ] diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6493bf088..5a6043441 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -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 { @@ -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(