news screen enhancement - 1

This commit is contained in:
Kazuma
2026-06-09 20:11:10 -04:00
parent 5655cde6bf
commit f0c794f0c0
13 changed files with 250 additions and 35 deletions
+38 -1
View File
@@ -1,7 +1,7 @@
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { ScreenerEngine } from './ScreenerEngine';
import { CatalystCache, SignalSnapshotRepository } from '../../domains/shared';
import type { LiveAssetResult, ScreenerResult } from '../../domains/shared';
import type { DataHealth, LiveAssetResult, ScreenerResult } from '../../domains/shared';
import { screenSchema } from '../../domains/shared/types/schemas';
export class ScreenerController {
@@ -65,11 +65,48 @@ export class ScreenerController {
const tickers = (req.body as { tickers: string[] }).tickers.map((t) => t.toUpperCase());
const results = await this.engine.screenTickers(tickers);
this.recordSnapshots(results, req);
const dataHealth = ScreenerController.assessDataHealth(results);
if (dataHealth.degraded) {
req.log?.warn?.({ dataHealth }, 'screen batch returned degraded fundamentals data');
}
return {
...results,
STOCK: ScreenerController.serializeAssets(results.STOCK as LiveAssetResult[]),
ETF: ScreenerController.serializeAssets(results.ETF as LiveAssetResult[]),
BOND: ScreenerController.serializeAssets(results.BOND as LiveAssetResult[]),
dataHealth,
};
}
/**
* P0.4 data-sanity sentinel — if a large share of screened stocks come back
* with null core fundamentals (P/E, ROE), the upstream source has likely
* changed schema or is throttling. Surface it loudly instead of letting
* everything silently degrade to "No Data" rows.
*/
private static assessDataHealth(results: ScreenerResult): DataHealth {
const THRESHOLD = 0.3; // >30% nulls = degraded
const MIN_SAMPLE = 3; // don't alarm on tiny batches
const stocks = results.STOCK as LiveAssetResult[];
const metrics = stocks.map(
(r) => r.asset.metrics as { peRatio?: number | null; returnOnEquity?: number | null },
);
const nullPeRatio = metrics.filter((m) => m.peRatio == null).length;
const nullRoe = metrics.filter((m) => m.returnOnEquity == null).length;
const total = metrics.length;
const degraded =
total >= MIN_SAMPLE && (nullPeRatio / total > THRESHOLD || nullRoe / total > THRESHOLD);
return {
degraded,
stocksChecked: total,
nullPeRatio,
nullRoe,
message: degraded
? `${Math.max(nullPeRatio, nullRoe)} of ${total} stocks returned no core fundamentals — data source may be degraded; treat this screen with caution`
: null,
};
}
@@ -151,6 +151,20 @@ export const WATCHLIST_QUERIES = {
`,
};
// ── Screening Universe Queries (bin/daily-screen.ts) ────────────────────────
export const UNIVERSE_QUERIES = {
// Every ticker pinned by any user
DISTINCT_WATCHLIST_TICKERS: 'SELECT DISTINCT ticker FROM watchlist ORDER BY ticker',
// Every ticker held by any user (crypto excluded — not fundamentally scored)
DISTINCT_HOLDING_TICKERS: `
SELECT DISTINCT ticker FROM holdings
WHERE type != 'crypto'
ORDER BY ticker
`,
};
// ── Signal Snapshot Queries (P0.1 — signal track record) ────────────────────
export const SIGNAL_SNAPSHOT_QUERIES = {
@@ -87,10 +87,26 @@ export interface AssetResult {
fundamental: ScoreResult;
}
/**
* Data-source health for one screen batch (PRODUCT.md P0.4).
* Degraded = a large share of stocks came back without core fundamentals,
* which usually means the upstream data source changed or is throttling —
* not that the companies are actually missing data.
*/
export interface DataHealth {
degraded: boolean;
stocksChecked: number;
nullPeRatio: number;
nullRoe: number;
message: string | null;
}
export interface ScreenerResult {
STOCK: AssetResult[];
ETF: AssetResult[];
BOND: AssetResult[];
ERROR: Array<{ ticker: string; message: string }>;
marketContext: import('./market.model.js').MarketContext;
/** Set by the screener controller on API responses, not by the engine. */
dataHealth?: DataHealth;
}
+2
View File
@@ -8,6 +8,8 @@ export type {
ScoringRules,
ScoreAudit,
ScoreResult,
VerdictTier,
DataHealth,
AssetResult,
LiveAssetResult,
ScreenerResult,