import test from 'node:test'; import assert from 'node:assert/strict'; import { ScreenerEngine } from '../server/domains/screener/ScreenerEngine.js'; import { BenchmarkProvider } from '../server/domains/shared/services/BenchmarkProvider.js'; import { noopLogger } from '../server/domains/shared/utils/logger.js'; import type { MarketContext } from '../server/domains/shared/types/market.model.js'; // Mock Yahoo Finance Client class MockYahooClient { async fetchSummary(ticker: string) { if (ticker === 'INVALID') { throw new Error('Not found'); } return { price: { regularMarketPrice: ticker === 'AAPL' ? 189.5 : 425.3, marketCap: ticker === 'AAPL' ? 2.8e12 : 1.6e12, }, summaryDetail: { fiftyTwoWeekHigh: ticker === 'AAPL' ? 199.62 : 468.5, fiftyTwoWeekLow: ticker === 'AAPL' ? 164.08 : 380.2, }, quoteType: { quoteType: 'EQUITY' }, defaultKeyStatistics: { trailingPE: ticker === 'AAPL' ? 28.5 : 32.1, }, financialData: { returnOnEquity: (ticker === 'AAPL' ? 95.2 : 48.5) / 100, operatingMargins: (ticker === 'AAPL' ? 30.7 : 27.8) / 100, grossMargins: (ticker === 'AAPL' ? 46.2 : 45.5) / 100, freeCashflow: ticker === 'AAPL' ? 110e9 : 60e9, totalRevenue: ticker === 'AAPL' ? 383.3e9 : 215.1e9, debtToEquity: ticker === 'AAPL' ? 75.56 : 55.2, currentRatio: ticker === 'AAPL' ? 0.95 : 1.2, }, incomeStatementHistoryQuarterly: { incomeStatementHistory: [ { commonStockSharesOutstanding: ticker === 'AAPL' ? 15.6e9 : 9.3e9, netIncome: ticker === 'AAPL' ? 25e9 : 20e9, }, ], }, }; } async fetchCalendarEvents() { return []; } async search() { return { quotes: [] }; } } class MockBenchmarkProvider extends BenchmarkProvider { async getMarketContext(): Promise { return { sp500Price: 5500, riskFreeRate: 4.5, vixLevel: 12.5, rateRegime: 'NORMAL', volatilityRegime: 'LOW', benchmarks: { marketPE: 20, techPE: 28, reitYield: 3.5, igSpread: 1.2, }, }; } } test('ScreenerEngine', async (t) => { await t.test('screenTickers() processes valid ticker', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers(['AAPL']); assert.ok(results); assert.ok('STOCK' in results); assert.ok('ETF' in results); assert.ok('BOND' in results); assert.ok('ERROR' in results); }); await t.test('screenTickers() handles error gracefully', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers(['INVALID']); assert.equal(results.ERROR.length, 1); assert.equal(results.ERROR[0].ticker, 'INVALID'); assert.ok(results.ERROR[0].message); }); await t.test('screenTickers() normalizes ticker to uppercase', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers(['aapl']); // Should process without error assert.ok(results); }); await t.test('screenTickers() batches requests with delay', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const startTime = Date.now(); await engine.screenTickers(['AAPL', 'MSFT']); const endTime = Date.now(); // Should have delay between batches (1000ms per batch) assert.ok(endTime - startTime >= 0); // At minimum, some time should pass }); await t.test('screenTickers() returns market context', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers(['AAPL']); assert.ok(results.marketContext); assert.equal(results.marketContext.sp500Price, 5500); assert.equal(results.marketContext.rateRegime, 'NORMAL'); }); await t.test('screenTickers() handles empty list', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers([]); assert.equal(results.STOCK.length, 0); assert.equal(results.ETF.length, 0); assert.equal(results.BOND.length, 0); assert.equal(results.ERROR.length, 0); }); await t.test('screenTickers() processes multiple tickers', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers(['AAPL', 'MSFT']); const totalResults = results.STOCK.length + results.ETF.length + results.BOND.length + results.ERROR.length; assert.equal(totalResults, 2); }); await t.test('screenWithProgress() works without logger', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenWithProgress(['AAPL']); assert.ok(results); assert.ok('marketContext' in results); }); await t.test('screenTickers() processes large ticker list correctly', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); // Create array of 10 tickers (batch size is 5, so should need 2 batches) const tickers = Array(10) .fill(0) .map((_, i) => (i % 2 === 0 ? 'AAPL' : 'MSFT')); const results = await engine.screenTickers(tickers); const totalResults = results.STOCK.length + results.ETF.length + results.BOND.length + results.ERROR.length; assert.equal(totalResults, 10); }); await t.test('screenTickers() includes scoring details', async () => { const client = new MockYahooClient(); const benchmark = new MockBenchmarkProvider(null as any); const engine = new ScreenerEngine(client as any, benchmark, { logger: noopLogger }); const results = await engine.screenTickers(['AAPL']); if (results.STOCK.length > 0) { const result = results.STOCK[0]; assert.ok(result.signal); assert.ok(result.fundamental); assert.ok(result.inflated); } }); });