86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
/**
|
|
* 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);
|