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 -19
View File
@@ -100,13 +100,11 @@ export class OptionsAdapter implements SourceFetch {
const yf = await this.yf();
if (kind === 'expiry_dates') {
// yahoo-finance2 v3: options() returns expirationDates as Date objects
const result = await yf.options(id);
const dates = (result.expirationDates ?? [])
.map((d: Date) => d.toISOString().slice(0, 10))
.sort() as OptionExpiryDate[];
// Use optionsExpiryDates() which returns string[] directly.
const dates = await yf.optionsExpiryDates(id);
const sorted = [...dates].sort() as OptionExpiryDate[];
return {
value: dates,
value: sorted,
ttlClass: 'intraday',
provenance: { fetchedAt, sourceKind: 'yfinance', rawSourceId: `options:expiry:${id}` },
};
@@ -114,14 +112,26 @@ export class OptionsAdapter implements SourceFetch {
if (kind === 'chain') {
const [symbol, expiry] = id.split(':');
const rawChain = await yf.options(symbol, { date: expiry });
// v3: options is indexed by number, extract calls/puts from first entry
const optionsObj = rawChain.options;
const firstKey = Object.keys(optionsObj)[0];
const expiryData = firstKey ? optionsObj[firstKey] : null;
const rawResult = await yf.options(symbol, expiry);
// Handle multiple response shapes:
// v3: { options: { '1234567890': { calls: [...], puts: [...] } } }
// flat: { calls: [...], puts: [...] }
let expiryData: Record<string, unknown> | null = null;
if (rawResult.options && typeof rawResult.options === 'object' && !Array.isArray(rawResult.options)) {
const optObj = rawResult.options as Record<string, unknown>;
const dateKeys = Object.keys(optObj).filter((k) => /^\d+$/.test(k) || /\d{4}-\d{2}-\d{2}/.test(k));
if (dateKeys.length > 0) {
expiryData = optObj[dateKeys[0]] as Record<string, unknown>;
} else {
expiryData = optObj;
}
} else if (Array.isArray(rawResult.calls) || Array.isArray(rawResult.puts)) {
// Flat format: { calls: [...], puts: [...] }
expiryData = rawResult as Record<string, unknown>;
}
const allRows = [
...(expiryData?.calls ?? []).map((c: Record<string, unknown>) => ({ ...c, right: 'call' as const })),
...(expiryData?.puts ?? []).map((p: Record<string, unknown>) => ({ ...p, right: 'put' as const })),
...((expiryData?.calls as Array<Record<string, unknown>> | undefined) ?? []).map((c) => ({ ...c, right: 'call' as const })),
...((expiryData?.puts as Array<Record<string, unknown>> | undefined) ?? []).map((p) => ({ ...p, right: 'put' as const })),
];
const rows = parseOptionChainRows(symbol, allRows);
return {
@@ -136,13 +146,23 @@ export class OptionsAdapter implements SourceFetch {
const symbol = parts[0];
const expiry = parts[1];
const strike = parseFloat(parts[2] ?? '0');
const rawChain = await yf.options(symbol, { date: expiry });
const optionsObj = rawChain.options;
const firstKey = Object.keys(optionsObj)[0];
const expiryData = firstKey ? optionsObj[firstKey] : null;
const rawResult = await yf.options(symbol, expiry);
// Handle multiple response shapes (same logic as chain kind).
let expiryData: Record<string, unknown> | null = null;
if (rawResult.options && typeof rawResult.options === 'object' && !Array.isArray(rawResult.options)) {
const optObj = rawResult.options as Record<string, unknown>;
const dateKeys = Object.keys(optObj).filter((k) => /^\d+$/.test(k) || /\d{4}-\d{2}-\d{2}/.test(k));
if (dateKeys.length > 0) {
expiryData = optObj[dateKeys[0]] as Record<string, unknown>;
} else {
expiryData = optObj;
}
} else if (Array.isArray(rawResult.calls) || Array.isArray(rawResult.puts)) {
expiryData = rawResult as Record<string, unknown>;
}
const allRows = [
...(expiryData?.calls ?? []).map((c: Record<string, unknown>) => ({ ...c, right: 'call' as const })),
...(expiryData?.puts ?? []).map((p: Record<string, unknown>) => ({ ...p, right: 'put' as const })),
...((expiryData?.calls as Array<Record<string, unknown>> | undefined) ?? []).map((c) => ({ ...c, right: 'call' as const })),
...((expiryData?.puts as Array<Record<string, unknown>> | undefined) ?? []).map((p) => ({ ...p, right: 'put' as const })),
];
const rows = parseOptionChainRows(symbol, allRows);
const target = strike > 0