news screen enhancement - 1
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user