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
+20 -12
View File
@@ -188,7 +188,7 @@ export class EdgarAdapter implements SourceFetch {
} else if (typeof recentObj === 'object') {
// Parallel arrays — convert to array of objects.
const keys = Object.keys(recentObj);
const len = (recentObj[keys[0]] as unknown[])?.length ?? 0;
const len = ((recentObj as Record<string, unknown>)[keys[0]] as unknown[])?.length ?? 0;
recentFilings = [];
for (let i = 0; i < len; i++) {
const row: Record<string, unknown> = {};
@@ -205,7 +205,7 @@ export class EdgarAdapter implements SourceFetch {
let filings = recentFilings;
if (opts?.formTypes && opts.formTypes.length > 0) {
const allowed = new Set(opts.formTypes.map((f) => f.toUpperCase()));
filings = filings.filter((f) => allowed.has((f.form ?? '').toUpperCase()));
filings = filings.filter((f) => allowed.has(String(f.form ?? '').toUpperCase()));
}
// Filter by date range.
@@ -213,7 +213,7 @@ export class EdgarAdapter implements SourceFetch {
const from = opts.dateRange.from ? new Date(opts.dateRange.from).getTime() : null;
const to = opts.dateRange.to ? new Date(opts.dateRange.to).getTime() : null;
filings = filings.filter((f) => {
const ts = new Date(f.reportDate ?? f.filingDate ?? f.dateReporter ?? '').getTime();
const ts = new Date(String(f.reportDate ?? f.filingDate ?? f.dateReporter ?? '')).getTime();
if (Number.isNaN(ts)) return false;
if (from !== null && ts < from) return false;
if (to !== null && ts > to) return false;
@@ -352,6 +352,7 @@ export class EdgarAdapter implements SourceFetch {
async form13f_holdings(
cik: string,
accession: string,
opts?: { limit?: number; offset?: number },
): Promise<FetchResult> {
const filerCik = accession.split('-')[0];
const padded = padCik(filerCik);
@@ -390,7 +391,7 @@ export class EdgarAdapter implements SourceFetch {
// Try each candidate until we find one with holdings; fall back to any non-HTML doc
let docText = '';
let parsed: Array<{ cusip: string; issuerName: string; value: number; sshPrnamt: number }> = [];
let parsed: Array<{ cusip: string; issuerName: string; value: number; sshPrnamt: number; putCall: string }> = [];
for (const cand of xmlCandidates) {
const docUrl = `https://www.sec.gov/Archives/edgar/data/${padded}/${accessionNoDashes}/${cand.name}`;
docText = await this.edgarXmlFetch(docUrl);
@@ -410,10 +411,14 @@ export class EdgarAdapter implements SourceFetch {
}
}
const holdings = parsed;
const allHoldings = parsed;
const total = allHoldings.length;
const offset = Math.max(0, opts?.offset ?? 0);
const limit = opts?.limit;
const holdings = limit != null ? allHoldings.slice(offset, offset + limit) : allHoldings;
return {
value: { holdings, accession: `${padded}/${accessionNoDashes}` },
value: { holdings, total, accession: `${padded}/${accessionNoDashes}` },
ttlClass: 'daily_permanent' as const,
provenance: {
fetchedAt: new Date().toISOString(),
@@ -581,10 +586,11 @@ function parse13fHoldings(text: string): Array<{
issuerName: string;
value: number;
sshPrnamt: number;
putCall: string;
}> {
const holdings: Array<{ cusip: string; issuerName: string; value: number; sshPrnamt: number }> = [];
const holdings: Array<{ cusip: string; issuerName: string; value: number; sshPrnamt: number; putCall: string }> = [];
// Try 13F XML format: <infoTable> (possibly namespaced) with <nameOfIssuer>, <cusip>, <value>, <sshPrnamt>
// Try 13F XML format: <infoTable> (possibly namespaced) with <nameOfIssuer>, <cusip>, <value>, <sshPrnamt>, <putCall>
// SEC 13F XML uses namespaces like <ns1:infoTable><ns1:nameOfIssuer>...
const infoTableMatches = text.match(/<(?:\w+:)?infoTable[^>]*>([\s\S]*?)<\/(?:\w+:)?infoTable>/gi);
if (infoTableMatches && infoTableMatches.length > 0) {
@@ -593,6 +599,7 @@ function parse13fHoldings(text: string): Array<{
const cusipMatch = block.match(/<(?:\w+:)?cusip>\s*([\s\S]*?)\s*<\/(?:\w+:)?cusip>/i);
const valueMatch = block.match(/<(?:\w+:)?value>\s*([\s\S]*?)\s*<\/(?:\w+:)?value>/i);
const sharesMatch = block.match(/<(?:\w+:)?sshPrnamt>\s*([\s\S]*?)\s*<\/(?:\w+:)?sshPrnamt>/i);
const putCallMatch = block.match(/<(?:\w+:)?putCall>\s*([\s\S]*?)\s*<\/(?:\w+:)?putCall>/i);
if (!cusipMatch) continue;
@@ -600,8 +607,9 @@ function parse13fHoldings(text: string): Array<{
const cusip = cusipMatch ? cusipMatch[1].replace(/<[^>]+>/g, '').trim() : '';
const value = valueMatch ? parseFloat(valueMatch[1].replace(/<[^>]+>/g, '').replace(/,/g, '')) || 0 : 0;
const sshPrnamt = sharesMatch ? parseFloat(sharesMatch[1].replace(/<[^>]+>/g, '').replace(/,/g, '')) || 0 : 0;
const putCall = putCallMatch ? putCallMatch[1].replace(/<[^>]+>/g, '').trim() : '';
holdings.push({ cusip, issuerName, value, sshPrnamt });
holdings.push({ cusip, issuerName, value, sshPrnamt, putCall });
}
if (holdings.length > 0) return holdings;
@@ -638,7 +646,7 @@ function parse13fHoldings(text: string): Array<{
const sharesMatch = cells[3].match(/([\d,.]+)/);
const sshPrnamt = sharesMatch ? parseFloat(sharesMatch[1].replace(/,/g, '')) : 0;
holdings.push({ cusip: cusipMatch[1], issuerName: issuerRaw, value, sshPrnamt });
holdings.push({ cusip: cusipMatch[1], issuerName: issuerRaw, value, sshPrnamt, putCall: '' });
}
}
}
@@ -657,7 +665,7 @@ function parse13fHoldings(text: string): Array<{
if (cusipLine && !/Class|CUSIP|Name|Issuer/i.test(line)) {
// Save previous holding if we have one.
if (currentCusip) {
holdings.push({ cusip: currentCusip, issuerName: currentIssuer, value: currentValue, sshPrnamt: currentShares });
holdings.push({ cusip: currentCusip, issuerName: currentIssuer, value: currentValue, sshPrnamt: currentShares, putCall: '' });
}
currentCusip = cusipLine[1];
currentIssuer = '';
@@ -681,7 +689,7 @@ function parse13fHoldings(text: string): Array<{
}
// Push last holding.
if (currentCusip) {
holdings.push({ cusip: currentCusip, issuerName: currentIssuer.trim(), value: currentValue, sshPrnamt: currentShares });
holdings.push({ cusip: currentCusip, issuerName: currentIssuer.trim(), value: currentValue, sshPrnamt: currentShares, putCall: '' });
}
}