import test from 'node:test'; import assert from 'node:assert/strict'; import { PortfolioAdvisor } from '../server/domains/portfolio/PortfolioAdvisor.js'; import { SIGNAL } from '../server/domains/shared/config/constants.js'; import type { PortfolioHolding, AdviceRow, } from '../server/domains/shared/types/portfolio.model.js'; import type { ScreenerResult } from '../server/domains/shared/types/asset.model.js'; class MockYahooClient { async fetchSummary(ticker: string) { const prices: Record = { AAPL: 189.5, MSFT: 425.3, 'BRK-B': 385.0, }; return { price: { regularMarketPrice: prices[ticker] || 100, marketCap: 1e12, }, summaryDetail: { fiftyTwoWeekHigh: (prices[ticker] || 100) * 1.1, fiftyTwoWeekLow: (prices[ticker] || 100) * 0.9, }, quoteType: { quoteType: 'EQUITY' }, defaultKeyStatistics: { trailingPE: 20 }, financialData: { returnOnEquity: 0.2, operatingMargins: 0.15, grossMargins: 0.4, freeCashflow: 50e9, totalRevenue: 200e9, debtToEquity: 50, currentRatio: 1.2, }, incomeStatementHistoryQuarterly: { incomeStatementHistory: [{ commonStockSharesOutstanding: 2.6e9, netIncome: 25e9 }], }, }; } normalise(ticker: string): string { return ticker.replace(/\./g, '-'); } } // Helper: build a minimal ScreenerResult with one STOCK result function makeScreenerResult(ticker: string, signal: string, price: number): ScreenerResult { return { STOCK: [ { signal, fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, asset: { ticker, currentPrice: price, type: 'STOCK', getDisplayMetrics: () => ({}), } as any, displayMetrics: {}, } as any, ], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }; } test('PortfolioAdvisor', async (t) => { await t.test('analyzes portfolio with single holding', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, ]; const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 1); }); await t.test('calculates position gain/loss correctly', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, ]; const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); const row = advice[0] as AdviceRow; // gain = 10 * (189.5 - 150) = $395 assert.ok(row.gainLossPct !== null); }); await t.test('handles empty portfolio', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const screened: ScreenerResult = { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }; const advice = await advisor.advise([], screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 0); }); await t.test('normalizes BRK.B to BRK-B', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'BRK.B', shares: 5, costBasis: 350, source: 'manual', type: 'stock' }, ]; const screened = makeScreenerResult('BRK-B', SIGNAL.NEUTRAL, 385.0); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 1); }); await t.test('analyzes multiple holdings', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, { ticker: 'MSFT', shares: 5, costBasis: 400, source: 'manual', type: 'stock' }, ]; const screened: ScreenerResult = { STOCK: [ { signal: SIGNAL.STRONG_BUY, fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, asset: { ticker: 'AAPL', currentPrice: 189.5, type: 'STOCK', getDisplayMetrics: () => ({}), } as any, displayMetrics: {}, } as any, { signal: SIGNAL.STRONG_BUY, fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, asset: { ticker: 'MSFT', currentPrice: 425.3, type: 'STOCK', getDisplayMetrics: () => ({}), } as any, displayMetrics: {}, } as any, ], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }; const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 2); }); await t.test('maps signal to advice recommendation', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, ]; for (const signal of [SIGNAL.STRONG_BUY, SIGNAL.NEUTRAL, SIGNAL.AVOID]) { const screened = makeScreenerResult('AAPL', signal, 189.5); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); } }); await t.test('handles fractional shares', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10.5, costBasis: 150, source: 'manual', type: 'stock' }, ]; const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 1); }); await t.test('returns advice rows with ticker information', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, ]; const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); const row = advice[0] as AdviceRow; assert.ok(row.ticker); }); await t.test('handles missing current price gracefully', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'UNKNOWN', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, ]; const screened: ScreenerResult = { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }; const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 1); assert.equal(advice[0].currentPrice, null); }); await t.test('calculates total portfolio value', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, { ticker: 'MSFT', shares: 5, costBasis: 400, source: 'manual', type: 'stock' }, ]; const screened: ScreenerResult = { STOCK: [ { signal: SIGNAL.STRONG_BUY, fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, asset: { ticker: 'AAPL', currentPrice: 189.5, type: 'STOCK', getDisplayMetrics: () => ({}), } as any, displayMetrics: {}, } as any, { signal: SIGNAL.STRONG_BUY, fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } }, asset: { ticker: 'MSFT', currentPrice: 425.3, type: 'STOCK', getDisplayMetrics: () => ({}), } as any, displayMetrics: {}, } as any, ], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }; const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); assert.equal(advice.length, 2); // AAPL: 10 * 189.5 = 1895, MSFT: 5 * 425.3 = 2126.5 const totalValue = advice.reduce((sum, r) => sum + parseFloat(r.marketValue ?? '0'), 0); assert.ok(totalValue > 0); }); await t.test('signals match in advice rows', async () => { const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const holdings: PortfolioHolding[] = [ { ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' }, ]; const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5); const advice = await advisor.advise(holdings, screened); assert.ok(Array.isArray(advice)); const row = advice[0] as AdviceRow; assert.equal(row.signal, SIGNAL.STRONG_BUY); }); });