diff --git a/apps/api/src/app/endpoints/ai/tools/portfolio-performance.tool.ts b/apps/api/src/app/endpoints/ai/tools/portfolio-performance.tool.ts index 9d91e695c..c7abf1c14 100644 --- a/apps/api/src/app/endpoints/ai/tools/portfolio-performance.tool.ts +++ b/apps/api/src/app/endpoints/ai/tools/portfolio-performance.tool.ts @@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; +import { subDays } from 'date-fns'; import { tool } from 'ai'; import { z } from 'zod'; @@ -58,30 +59,7 @@ export function getPortfolioPerformanceTool(deps: { }); } -function computeAllTimePerformance( - deps: { dataProviderService: DataProviderService }, - orders: { - type: string; - quantity: number; - unitPrice: number; - date: Date; - symbolProfileId: string; - }[], - profileMap: Record< - string, - { - id: string; - symbol: string; - dataSource: any; - name: string; - currency: string; - } - > -) { - return computePerformanceFromOrders(deps, orders, profileMap); -} - -async function computePerformanceFromOrders( +async function computeAllTimePerformance( deps: { dataProviderService: DataProviderService }, orders: { type: string; @@ -224,7 +202,6 @@ async function computePeriodPerformance( dateRange: "1d" | "mtd" | "wtd" | "ytd" | "1y" | "5y" ) { const { startDate, endDate } = getIntervalFromDateRange(dateRange); - const warnings: string[] = []; // Partition orders into before-period and during-period const ordersBeforePeriod = orders.filter( @@ -237,7 +214,13 @@ async function computePeriodPerformance( // Build positions at start of period from orders before period const startPositions: Record< string, - { quantity: number; costBasis: number; symbol: string; dataSource: any; name: string } + { + quantity: number; + costBasis: number; + symbol: string; + dataSource: any; + name: string; + } > = {}; for (const order of ordersBeforePeriod) { @@ -262,28 +245,50 @@ async function computePeriodPerformance( } } - // Look up historical prices on/before startDate for each starting position - let startValue = 0; const activeStartPositions = Object.values(startPositions).filter( (p) => p.quantity > 0 ); - for (const pos of activeStartPositions) { - const historicalPrice = await deps.prismaService.marketData.findFirst({ - where: { - symbol: pos.symbol, - dataSource: pos.dataSource, - date: { lte: startDate } - }, - orderBy: { date: "desc" }, - select: { marketPrice: true } + // Fetch historical prices from the data provider (Yahoo Finance) for the + // period start date. Use a 7-day window to account for weekends/holidays. + const startPriceMap: Record = {}; + + if (activeStartPositions.length > 0) { + const items = activeStartPositions.map((p) => ({ + dataSource: p.dataSource, + symbol: p.symbol + })); + + const historicalData = await deps.dataProviderService.getHistoricalRaw({ + assetProfileIdentifiers: items, + from: subDays(startDate, 7), + to: startDate }); - if (historicalPrice) { - startValue += - pos.quantity * (historicalPrice.marketPrice as unknown as number); + for (const pos of activeStartPositions) { + const symbolData = historicalData[pos.symbol]; + if (symbolData) { + // Get the most recent price in the window (closest to startDate) + const dates = Object.keys(symbolData).sort(); + const latestDate = dates[dates.length - 1]; + if (latestDate && symbolData[latestDate]?.marketPrice != null) { + startPriceMap[pos.symbol] = Number( + symbolData[latestDate].marketPrice + ); + } + } + } + } + + // Compute startValue using historical prices, with cost-basis fallback + let startValue = 0; + const warnings: string[] = []; + + for (const pos of activeStartPositions) { + const historicalPrice = startPriceMap[pos.symbol]; + if (historicalPrice != null) { + startValue += pos.quantity * historicalPrice; } else { - // Fall back to cost basis startValue += pos.costBasis; warnings.push( `No historical price found for ${pos.symbol} at period start; using cost basis as fallback.` @@ -297,7 +302,6 @@ async function computePeriodPerformance( { quantity: number; symbol: string; dataSource: any; name: string } > = {}; - // Start with positions from before the period for (const pos of activeStartPositions) { endPositions[pos.symbol] = { quantity: pos.quantity, @@ -307,7 +311,7 @@ async function computePeriodPerformance( }; } - let netCashFlow = 0; // positive = money added (buys), negative = money removed (sells) + let netCashFlow = 0; for (const order of ordersDuringPeriod) { const profile = profileMap[order.symbolProfileId]; @@ -377,7 +381,6 @@ async function computePeriodPerformance( } } - // Period gain = endValue - startValue - netCashFlow const periodGain = endValue - startValue - netCashFlow; const denominator = startValue > 0 ? startValue : Math.abs(netCashFlow); const periodGainPercentage =