phase-10.5: screener enhancements
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* One-shot news poll — for cron users who don't run the server 24/7.
|
||||
* Fetches EDGAR + PR-wire feeds once, runs the pipeline, runs retention,
|
||||
* prints stats, exits.
|
||||
*
|
||||
* Usage:
|
||||
* npm run news:poll
|
||||
*
|
||||
* Crontab example (every 15 min, market hours, weekdays):
|
||||
* *\/15 9-16 * * 1-5 cd /path/to/market_screener && npm run news:poll
|
||||
*
|
||||
* If the server runs continuously, its built-in scheduler covers this —
|
||||
* set NEWS_POLL=off on the server if you prefer cron-driven polling.
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { createDb, DatabaseConnection, QueryAudit, noopLogger } from '../server/domains/shared';
|
||||
import {
|
||||
NewsRepository,
|
||||
NewsPipeline,
|
||||
UniverseProvider,
|
||||
NewsScheduler,
|
||||
EdgarPoller,
|
||||
PrWirePoller,
|
||||
} from '../server/domains/news';
|
||||
|
||||
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 universe = new UniverseProvider(db);
|
||||
const pipeline = new NewsPipeline(new NewsRepository(db));
|
||||
const scheduler = new NewsScheduler(
|
||||
pipeline,
|
||||
universe,
|
||||
new EdgarPoller(noopLogger),
|
||||
new PrWirePoller(noopLogger),
|
||||
consoleLogger,
|
||||
);
|
||||
|
||||
const size = universe.getUniverse().size;
|
||||
if (size === 0) {
|
||||
console.log('Universe is empty (no watchlist, holdings, or recent screens) — nothing to poll.'); // eslint-disable-line no-console
|
||||
process.exit(0);
|
||||
}
|
||||
console.log(`Polling news for a ${size}-ticker universe…`); // eslint-disable-line no-console
|
||||
|
||||
try {
|
||||
const { edgar, prwire } = await scheduler.runOnce();
|
||||
const retention = pipeline.runRetention();
|
||||
/* eslint-disable no-console */
|
||||
console.log('\nEDGAR :', JSON.stringify(edgar));
|
||||
console.log('PR-wire:', JSON.stringify(prwire));
|
||||
console.log('Retention:', JSON.stringify(retention));
|
||||
/* eslint-enable no-console */
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('News poll failed:', (err as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Discord webhook smoke test — sends a FAKE digest to DISCORD_WEBHOOK_URL
|
||||
* so you can verify the integration without waiting for a real signal change.
|
||||
*
|
||||
* Usage:
|
||||
* npm run discord:test
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { DiscordNotifier } from '../server/domains/digest/DiscordNotifier';
|
||||
import type { DigestReport } from '../server/domains/shared/types';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
if (!process.env.DISCORD_WEBHOOK_URL) {
|
||||
console.error('DISCORD_WEBHOOK_URL is not set in .env');
|
||||
console.error('Discord → channel → Settings → Integrations → Webhooks → New Webhook → Copy URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fakeReport: DigestReport = {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
snapshotCount: 3,
|
||||
newTickers: [],
|
||||
changes: [
|
||||
{
|
||||
ticker: 'TEST',
|
||||
previousSignal: '✅ Strong Buy',
|
||||
newSignal: '🔄 Neutral',
|
||||
previousDate: 'yesterday',
|
||||
scoreDelta: -7,
|
||||
price: 123.45,
|
||||
catalysts: [
|
||||
{
|
||||
headline: '🔧 This is a TEST message from market-screener — webhook works!',
|
||||
catalyst: 'regulatory',
|
||||
source: 'edgar',
|
||||
url: 'https://example.com',
|
||||
publishedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
maStories: [
|
||||
{
|
||||
headline: '🔧 TEST: SC 13D filing example (M&A section renders like this)',
|
||||
catalyst: 'ma',
|
||||
source: 'edgar',
|
||||
url: 'https://example.com',
|
||||
publishedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const logger = {
|
||||
log: (...args: unknown[]) => console.log(...args),
|
||||
warn: (...args: unknown[]) => console.warn(...args),
|
||||
write: (msg: string) => process.stdout.write(msg),
|
||||
};
|
||||
|
||||
const ok = await new DiscordNotifier(logger).send(fakeReport);
|
||||
if (ok) {
|
||||
console.log('✓ Test digest posted — check your Discord channel.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error('✗ Post failed. Check the webhook URL (it may have been deleted/regenerated).');
|
||||
process.exit(1);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
Reference in New Issue
Block a user