diff --git a/app/server/src/cache/CacheRepository.ts b/app/server/src/cache/CacheRepository.ts index 5fb2fe5..c2cd3c5 100644 --- a/app/server/src/cache/CacheRepository.ts +++ b/app/server/src/cache/CacheRepository.ts @@ -19,6 +19,7 @@ export interface Provenance { fetchedAt: string; sourceKind: SourceKind; rawSour export interface Quote { symbol: string; price: number; bid?: number | null; ask?: number | null; change?: number | null; changePercent?: number | null; iv?: number | null; } export interface PriceCandle { ts: string; o: number; h: number; l: number; c: number; v: number; adjClose?: number | null; } export interface SymbolMeta { symbol: string; name?: string | null; sector?: string | null; industry?: string | null; exchange?: string | null; tickerKind: TickerKind; peers?: string[] | null; } +export interface PriceAdjustment { symbol: string; exDate: string; type: "split" | "dividend"; ratio: number } /** Port CacheRepository depends on to schedule background refreshes. SourceAdapter/AdapterQueue satisfy this. */ export interface CacheScheduler { queue(key: CacheKey): Promise; } @@ -103,6 +104,21 @@ const candlesHandler: KindHandler = { isStale(ts) { return ts === null; }, // permanent: stale only when absent }; +const adjustmentsHandler: KindHandler = { + ttlClass: 'daily_permanent', + read(d, symbol) { + const rows = d.prepare('SELECT ex_date, type, ratio FROM price_adjustments WHERE symbol=? ORDER BY ex_date ASC').all(symbol) as Array>; + if (!rows.length) return null; + const value: PriceAdjustment[] = rows.map((r) => ({ symbol, exDate: r.ex_date as string, type: r.type as "split" | "dividend", ratio: r.ratio as number })); + return { value, stalenessTs: rows[rows.length - 1].ex_date as string }; + }, + write(d, _symbol, value, provenance) { + const ins = d.prepare('INSERT OR REPLACE INTO price_adjustments (symbol, ex_date, type, ratio) VALUES (?,?,?,?)'); + for (const a of value as PriceAdjustment[]) ins.run(a.symbol, a.exDate, a.type, a.ratio); + }, + isStale(ts) { return ts === null; }, // permanent: stale only when absent +}; + const symbolHandler: KindHandler = { ttlClass: 'symbol_meta', read(d, symbol) { @@ -127,6 +143,7 @@ const HANDLERS = new Map([ ['quote', quoteHandler], ['candles', candlesHandler], ['symbol', symbolHandler], + ['adjustments', adjustmentsHandler], ]); export interface CacheRepository { @@ -185,8 +202,8 @@ export class CacheRepositoryImpl implements CacheRepository { const before = prev?.refcount ?? 0; d.prepare('UPDATE symbol_demand SET refcount = refcount + 1, in_demand = 1 WHERE symbol=?').run(symbol); if (before === 0) { - // First demand: schedule initial cache population (slice 1: yfinance quote + symbol meta) - for (const k of [`yfinance:quote:${symbol}`, `yfinance:symbol:${symbol}`]) { + // First demand: schedule initial cache population (slice 1: yfinance quote + symbol meta + candles + adjustments) + for (const k of [`yfinance:quote:${symbol}`, `yfinance:symbol:${symbol}`, `yfinance:candles:${symbol}:1d`, `yfinance:adjustments:${symbol}`]) { try { await this._scheduler.queue(k); } catch { /* ignore */ } } }