Files
market_screener/server/domains/shared/persistence/SignalSnapshotRepository.ts
T
saikiranvella 662a717916 UI enhancemnts
2026-06-09 19:34:31 -04:00

91 lines
2.9 KiB
TypeScript

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<SignalSnapshotRow>(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<SignalSnapshotRow>(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<SignalSnapshotRow>(qb);
}
private static today(): string {
return new Date().toISOString().slice(0, 10);
}
}