Files
market_screener/bin/daily-screen.ts
T
2026-06-09 20:12:37 -04:00

93 lines
3.1 KiB
TypeScript

/**
* 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<string, number>();
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);
}