import { DatabaseConnection } from '../shared/db/index'; import { QueryBuilder } from '../shared/utils/QueryBuilder'; /** * The tracked-ticker universe (FREE-DATA-STACK §4.1): * watchlist ∪ holdings ∪ tickers screened in the last 30 days. * * This is the news pipeline's first and biggest filter — stories about * tickers outside the universe are never stored. Cached for 10 minutes; * the universe changes slowly. */ export class UniverseProvider { private static readonly CACHE_TTL_MS = 10 * 60 * 1000; private static readonly SNAPSHOT_LOOKBACK_DAYS = 30; private cache: { universe: Set; expiresAt: number } = { universe: new Set(), expiresAt: 0, }; constructor(private readonly db: DatabaseConnection) {} getUniverse(): Set { if (Date.now() < this.cache.expiresAt) return this.cache.universe; const sinceDay = new Date( Date.now() - UniverseProvider.SNAPSHOT_LOOKBACK_DAYS * 24 * 60 * 60 * 1000, ) .toISOString() .slice(0, 10); const tickers = new Set(); const add = (rows: { ticker: string }[]) => rows.forEach((r) => tickers.add(r.ticker.toUpperCase())); add(this.db.all(new QueryBuilder('UNIVERSE_QUERIES.DISTINCT_WATCHLIST_TICKERS'))); add(this.db.all(new QueryBuilder('UNIVERSE_QUERIES.DISTINCT_HOLDING_TICKERS'))); add( this.db.all(new QueryBuilder('UNIVERSE_QUERIES.DISTINCT_SNAPSHOT_TICKERS_SINCE', [sinceDay])), ); this.cache = { universe: tickers, expiresAt: Date.now() + UniverseProvider.CACHE_TTL_MS }; return tickers; } /** Force next getUniverse() to re-read (e.g. after a watchlist change). */ invalidate(): void { this.cache.expiresAt = 0; } }