/** * Daily change digest (PRODUCT.md P1.1) — diff today's signal snapshots * against the previous ones, join with stored news catalysts, and post to * Discord (DISCORD_WEBHOOK_URL) or print to the terminal. * * RUN ORDER MATTERS — screen first, digest second: * 30 16 * * 1-5 cd /path/to/app && npm run screen:daily && npm run digest:daily * * Usage: * npm run digest:daily # today * npm run digest:daily -- 2026-06-09 # specific day */ import 'dotenv/config'; import { createDb, DatabaseConnection, QueryAudit, SignalSnapshotRepository, } from '../server/domains/shared'; import { NewsRepository } from '../server/domains/news'; import { DigestService, DiscordNotifier } from '../server/domains/digest'; const db = new DatabaseConnection(createDb(process.env.DB_PATH ?? './market-screener.db'), { audit: new QueryAudit(), logSlowQueries: 100, }); const consoleLogger = { log: (...args: unknown[]) => console.log(...args), // eslint-disable-line no-console warn: (...args: unknown[]) => console.warn(...args), write: (msg: string) => process.stdout.write(msg), }; const dateArg = process.argv[2]; const date = dateArg && /^\d{4}-\d{2}-\d{2}$/.test(dateArg) ? dateArg : new Date().toISOString().slice(0, 10); const digest = new DigestService(new SignalSnapshotRepository(db), new NewsRepository(db)); const report = digest.build(date); /* eslint-disable no-console */ console.log(`\nšŸ“Š Daily Signal Digest — ${report.date}`); console.log(`Tickers snapshotted: ${report.snapshotCount}`); if (report.snapshotCount === 0) { console.log('\nNo snapshots for this date. Run `npm run screen:daily` first.'); process.exit(0); } if (report.changes.length === 0) { console.log('No signal changes since the previous snapshots. Calm day.'); } else { console.log(`\nSignal changes (${report.changes.length}):`); for (const c of report.changes) { const delta = c.scoreDelta != null ? ` (score ${c.scoreDelta > 0 ? '+' : ''}${c.scoreDelta})` : ''; console.log(`\n ${c.ticker}: ${c.previousSignal} → ${c.newSignal}${delta}`); if (c.catalysts.length === 0) { console.log(' no catalyst found — moved on fundamentals/market data'); } for (const s of c.catalysts.slice(0, 3)) { console.log(` [${s.catalyst ?? 'news'}] ${s.headline}`); } } } if (report.maStories.length > 0) { console.log(`\nšŸ”± M&A activity (${report.maStories.length}):`); for (const s of report.maStories.slice(0, 5)) console.log(` • ${s.headline}`); } if (report.newTickers.length > 0) { console.log(`\nFirst-time snapshots (no baseline yet): ${report.newTickers.join(', ')}`); } const notifier = new DiscordNotifier(consoleLogger); if (notifier.enabled) { const sent = await notifier.send(report); console.log(sent ? '\nPosted to Discord āœ“' : '\nDiscord post skipped/failed'); } else { console.log('\n(Set DISCORD_WEBHOOK_URL in .env to receive this as a Discord message.)'); } /* eslint-enable no-console */ process.exit(0);