import { DatabaseConnection } from '../db/index'; import { QueryBuilder } from '../utils/QueryBuilder'; import type { ScoreResult, SignalSnapshotRow } from '../types'; /** * Signal snapshot ledger (PRODUCT.md P0.1). * * Persists one row per ticker per day on every /api/screen call so the * product builds a verifiable signal track record. This data cannot be * backfilled — the backtest dashboard (Phase 10.5e), thesis review (10.6d), * and calibration features all depend on it accumulating from day one. * * Recording is best-effort: failures are logged by the caller and must never * fail the screen request itself. */ export interface SnapshotInput { ticker: string; assetType: string; price: number | null; signal: string; fundamental: ScoreResult; inflated: ScoreResult; rateRegime?: string | null; } export class SignalSnapshotRepository { constructor(private readonly db: DatabaseConnection) {} /** * Upsert today's snapshot for a batch of screened assets. * Repeated screens on the same day keep the latest result. */ recordBatch(inputs: SnapshotInput[], date = SignalSnapshotRepository.today()): number { let written = 0; for (const input of inputs) { this.record(input, date); written++; } return written; } record(input: SnapshotInput, date = SignalSnapshotRepository.today()): void { const { ticker, assetType, price, signal, fundamental, inflated, rateRegime } = input; const coverage = fundamental.audit?.coverage ?? inflated.audit?.coverage ?? null; const riskFlags = fundamental.audit?.riskFlags ?? inflated.audit?.riskFlags ?? null; const qb = new QueryBuilder('SIGNAL_SNAPSHOT_QUERIES.UPSERT', [ ticker.toUpperCase(), date, assetType, price, signal, fundamental.tier, fundamental.score, fundamental.label, inflated.tier, inflated.score, inflated.label, coverage?.active ?? null, coverage?.total ?? null, riskFlags ? JSON.stringify(riskFlags) : null, rateRegime ?? null, new Date().toISOString(), ]); this.db.run(qb); } /** Full history for one ticker, oldest first. */ history(ticker: string): SignalSnapshotRow[] { const qb = new QueryBuilder('SIGNAL_SNAPSHOT_QUERIES.SELECT_BY_TICKER', [ticker.toUpperCase()]); return this.db.all(qb); } /** All snapshots for a given day (YYYY-MM-DD). */ byDate(date: string): SignalSnapshotRow[] { const qb = new QueryBuilder('SIGNAL_SNAPSHOT_QUERIES.SELECT_BY_DATE', [date]); return this.db.all(qb); } /** Latest snapshot per ticker strictly before a date — for daily diffing. */ latestBefore(date: string): SignalSnapshotRow[] { const qb = new QueryBuilder('SIGNAL_SNAPSHOT_QUERIES.SELECT_LATEST_BEFORE', [date]); return this.db.all(qb); } private static today(): string { return new Date().toISOString().slice(0, 10); } }