fix (ornith-35): watchlistRepository double-encoding bug — single JSON.stringify, 13/13 tests pass

This commit is contained in:
Investor Flow Build
2026-06-30 17:54:01 -04:00
parent 97607e0bd4
commit 1007ab4ed5
62 changed files with 11617 additions and 34 deletions
+68
View File
@@ -139,11 +139,79 @@ const symbolHandler: KindHandler = {
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.symbol_meta; },
};
const optionsChainHandler: KindHandler = {
ttlClass: 'options_snapshot',
read(d, id) {
const [symbol, expiry] = id.split(':');
if (!expiry) return null;
const rows = d.prepare(
'SELECT symbol,expiry,strike,type,bid,ask,iv,delta,gamma,theta,vega,open_interest,volume,ts FROM options_chains WHERE symbol=? AND expiry=? ORDER BY strike ASC, type ASC'
).all(symbol, expiry) as Array<Record<string, unknown>>;
if (!rows.length) return null;
const value = rows.map((r) => ({
contractSymbol: `${r.symbol}_${r.expiry}_${r.strike}_${r.type}`,
strike: r.strike as number,
right: r.type as 'call' | 'put',
expiration: r.expiry as string,
bid: r.bid as number | null,
ask: r.ask as number | null,
impliedVolatility: r.iv as number | null,
delta: r.delta as number | null,
gamma: r.gamma as number | null,
theta: r.theta as number | null,
vega: r.vega as number | null,
openInterest: r.open_interest as number | null,
volume: r.volume as number | null,
}));
return { value, stalenessTs: rows[rows.length - 1].ts as string };
},
write(d, id, value, provenance) {
const [symbol, expiry] = id.split(':');
if (!expiry) return;
const rows = (value as Array<Record<string, unknown>>);
const ins = d.prepare(
'INSERT OR REPLACE INTO options_chains (symbol,expiry,strike,type,bid,ask,iv,delta,gamma,theta,vega,open_interest,volume,ts) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
);
for (const r of rows) {
const strike = typeof r.strike === 'number' ? r.strike : 0;
const right = r.right === 'put' ? 'put' : 'call';
ins.run(
symbol, expiry, strike, right,
r.bid ?? null, r.ask ?? null, r.impliedVolatility ?? null,
r.delta ?? null, r.gamma ?? null, r.theta ?? null, r.vega ?? null,
r.openInterest ?? null, r.volume ?? null,
provenance.fetchedAt
);
}
},
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.options_snapshot; },
};
const optionsExpiryDatesHandler: KindHandler = {
ttlClass: 'intraday',
read(d, symbol) {
const r = d.prepare('SELECT value, observed_at FROM kv_cache WHERE key=?').get(`options_expiry:${symbol}`) as Record<string, unknown> | undefined;
if (!r) return null;
try {
const value = JSON.parse(r.value as string);
return { value, stalenessTs: r.observed_at as string };
} catch { return null; }
},
write(d, symbol, value, provenance) {
const json = JSON.stringify(value);
d.prepare('INSERT OR REPLACE INTO kv_cache (key, value, observed_at) VALUES (?,?,?)')
.run(`options_expiry:${symbol}`, json, provenance.fetchedAt);
},
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.intraday; },
};
const HANDLERS = new Map<string, KindHandler>([
['quote', quoteHandler],
['candles', candlesHandler],
['symbol', symbolHandler],
['adjustments', adjustmentsHandler],
['chain', optionsChainHandler],
['expiry_dates', optionsExpiryDatesHandler],
]);
export interface CacheRepository {