fix: backfill symbol_demand for sidebar-added symbols + analyst ratings schema fix

- Add await ctx.cache.subscribe() to addSymbol mutation so symbols
  added via the sidebar get registered in symbol_demand and yfinance
  jobs are queued immediately
- Backfill PEP, WYNN, STZ, CELH into symbol_demand + adapter_queue
- Upgrade yahoo-finance2 3.15.3 -> 3.15.4 and pass validateResult:false
  to quoteSummary() to handle Yahoo schema drift
- Add error detail logging for analyst ratings schema failures
- Update .gitignore with common ignores
This commit is contained in:
Investor Flow Build
2026-07-23 18:02:24 -04:00
parent 5ef2b2f060
commit e262187c3c
204 changed files with 25014 additions and 2934 deletions
+39 -2
View File
@@ -6,7 +6,7 @@
import { DatabaseSync } from 'node:sqlite';
import { db as defaultDb } from '../db/client.ts';
export type SourceKind = 'yfinance' | 'sec' | 'sec-fetch' | 'reddit' | 'x' | 'macro' | 'llm' | 'sec-lint-holders' | 'sec-lint-insiders';
export type SourceKind = 'yfinance' | 'sec' | 'sec-fetch' | 'reddit' | 'x' | 'macro' | 'llm' | 'sec-lint-holders' | 'sec-lint-insiders' | 'fred';
export type TickerKind = 'equity' | 'crypto' | 'etf' | 'index';
export type CacheKey = string; // `${SourceKind}:${kind}:${id}` e.g. 'yfinance:quote:NVDA', 'yfinance:candles:NVDA:1d'
export type TtlClass =
@@ -18,7 +18,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 SymbolMeta { symbol: string; name?: string | null; sector?: string | null; industry?: string | null; exchange?: string | null; tickerKind: TickerKind; peers?: string[] | null; description?: string | null; website?: string | null; marketCap?: number | null; currency?: string | null; employees?: number | null; country?: string | null; city?: 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. */
@@ -308,6 +308,8 @@ export interface CacheRepository {
unsubscribe(symbol: string): Promise<void>;
demandSet(): Promise<string[]>;
getMany<T>(keys: CacheKey[]): Promise<Array<{ key: CacheKey; value: T | null; isStale: boolean }>>;
/** Delete a cache entry by key (or, for wildcard keys ending in `:*`, all matching entries). */
del(key: CacheKey): Promise<void>;
}
export class CacheRepositoryImpl implements CacheRepository {
@@ -376,6 +378,41 @@ export class CacheRepositoryImpl implements CacheRepository {
return { key, value: e.value, isStale: e.isStale };
}));
}
async del(key: CacheKey): Promise<void> {
const { source, kind, id } = parseCacheKey(key);
const d = this._db;
switch (kind) {
case 'quote':
d.prepare('DELETE FROM quotes WHERE symbol=?').run(id);
break;
case 'candles': {
const [symbol, tf] = id.split(':');
d.prepare('DELETE FROM price_candles WHERE symbol=? AND timeframe=?').run(symbol, tf);
break;
}
case 'symbol':
d.prepare('DELETE FROM symbols WHERE symbol=?').run(id);
break;
case 'adjustments':
d.prepare('DELETE FROM price_adjustments WHERE symbol=?').run(id);
break;
case 'chain':
case 'greeks': {
const [symbol, expiry] = id.split(':');
d.prepare('DELETE FROM options_chains WHERE symbol=? AND expiry=?').run(symbol, expiry);
break;
}
case 'expiry_dates':
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`options_expiry:${id}`);
break;
default: {
// Unknown/wildcard kind (e.g. `x:cashtag:*`): best-effort delete from kv_cache via LIKE.
const like = key.endsWith(':*') ? `${key.slice(0, -1)}%` : key;
d.prepare('DELETE FROM kv_cache WHERE key LIKE ?').run(like);
}
}
void source;
}
}
export function createCacheRepository(opts: { db: DatabaseSync; scheduler: CacheScheduler }): CacheRepository {