/** * Daily screening job — keeps the signal snapshot ledger (PRODUCT.md P0.1) * accumulating even when nobody opens the UI. * * Universe = union of all users' watchlist tickers + all non-crypto holdings, * or an explicit list passed on the command line. * * Usage: * npm run screen:daily # watchlist + holdings universe * npm run screen:daily -- AAPL MSFT # explicit tickers * * Schedule for market close, e.g. crontab (4:30pm ET weekdays): * 30 16 * * 1-5 cd /path/to/market_screener && npm run screen:daily */ import 'dotenv/config'; import { YahooFinanceClient, BenchmarkProvider, SignalSnapshotRepository, createDb, DatabaseConnection, QueryAudit, } from '../server/domains/shared'; import { QueryBuilder } from '../server/domains/shared/utils/QueryBuilder'; import { ScreenerEngine } from '../server/domains/screener'; import type { AssetResult } from '../server/domains/shared'; function universeFromDb(db: DatabaseConnection): string[] { const watchlist = db .all<{ ticker: string }>(new QueryBuilder('UNIVERSE_QUERIES.DISTINCT_WATCHLIST_TICKERS')) .map((r) => r.ticker); const holdings = db .all<{ ticker: string }>(new QueryBuilder('UNIVERSE_QUERIES.DISTINCT_HOLDING_TICKERS')) .map((r) => r.ticker); return [...new Set([...watchlist, ...holdings])].sort(); } const db = new DatabaseConnection(createDb(process.env.DB_PATH ?? './market-screener.db'), { audit: new QueryAudit(), logSlowQueries: 100, }); const cliTickers = process.argv.slice(2).map((t) => t.toUpperCase()); const tickers = cliTickers.length > 0 ? cliTickers : universeFromDb(db); if (tickers.length === 0) { console.log('No tickers to screen — watchlist and holdings are empty.'); console.log('Pass tickers explicitly: npm run screen:daily -- AAPL MSFT'); process.exit(0); } console.log(`Screening ${tickers.length} tickers: ${tickers.join(', ')}`); const yahoo = new YahooFinanceClient(); const benchmark = new BenchmarkProvider(yahoo); const engine = new ScreenerEngine(yahoo, benchmark); const snapshots = new SignalSnapshotRepository(db); try { const results = await engine.screenWithProgress(tickers); const rateRegime = results.marketContext?.rateRegime ?? null; const assets = [...results.STOCK, ...results.ETF, ...results.BOND] as AssetResult[]; const written = snapshots.recordBatch( assets.map((r) => ({ ticker: r.asset.ticker, assetType: r.asset.type, price: r.asset.currentPrice ?? null, signal: r.signal, fundamental: r.fundamental, inflated: r.inflated, rateRegime, })), ); const bySignal = new Map(); for (const a of assets) bySignal.set(a.signal, (bySignal.get(a.signal) ?? 0) + 1); console.log(`\nSnapshots written: ${written}`); for (const [signal, count] of [...bySignal.entries()].sort()) { console.log(` ${signal}: ${count}`); } if (results.ERROR.length > 0) { console.log(`Errors (${results.ERROR.length}):`); for (const e of results.ERROR) console.log(` ${e.ticker}: ${e.message}`); } process.exit(0); } catch (err) { console.error('Daily screen failed:', (err as Error).message); process.exit(1); }