197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
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<MarketContext> {
|
|
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);
|
|
}
|
|
});
|
|
});
|