#!/usr/bin/env -S node --experimental-strip-types /** * Q2 13F deadline helper. * * node --experimental-strip-types scripts/sync-13f-deadline.ts * node --experimental-strip-types scripts/sync-13f-deadline.ts --watch * * Syncs every enabled tracked fund (Alpine Fox first), then requeues * institution-filing refreshes for open portfolio symbols. * --watch keeps polling tracked-fund 13Fs until 2026-Q2 lands or 23:59 ET. */ import { createDb } from '../src/db/client.ts'; import { queueSecFetch } from '../src/admin/admin.ts'; import { fetchFund13F } from '../src/mirror/fund13fFetcher.ts'; import { seedCuratedCusips } from '../src/services/cusipRegistry.ts'; const TARGET_QUARTER = '2026-Q2'; const POLL_MS = 15 * 60_000; function hasQuarter(db: ReturnType, cik: string, quarter: string): boolean { const row = db .prepare( `SELECT COUNT(*) AS n FROM institution_filings WHERE filer_cik IN (?, ?) AND form='13F-HR' AND reported_quarter=?`, ) .get(cik, cik.replace(/^0+/, ''), quarter) as { n: number }; return row.n > 0; } async function syncFunds(db: ReturnType): Promise<{ funds: Array<{ id: string; name: string; cik: string; result: unknown; hasTarget: boolean }>; }> { const funds = db .prepare(`SELECT id, fund_name, ci_key FROM tracked_funds WHERE enabled=1 ORDER BY fund_name`) .all() as Array<{ id: string; fund_name: string; ci_key: string }>; const out = []; for (const f of funds) { if (!/^\d+$/.test(f.ci_key.replace(/^0+/, '')) && !/^\d{10}$/.test(f.ci_key)) { console.log(`[skip] ${f.fund_name} has no SEC CIK (${f.ci_key})`); continue; } console.log(`[fund] syncing ${f.fund_name} (${f.ci_key})…`); try { const result = await fetchFund13F(db, f.id); const hasTarget = hasQuarter(db, f.ci_key, TARGET_QUARTER); console.log( `[fund] ${f.fund_name}: filings=${result.fetched_filings} holdings=${result.holdings_seen} upserted=${result.symbols_upserted} records=${result.records_inserted} ${TARGET_QUARTER}=${hasTarget ? 'YES' : 'not yet'}`, ); if (result.unresolved.length) { const uniq = [...new Map(result.unresolved.map((u) => [u.cusip, u])).values()]; console.log(`[fund] unresolved CUSIPs (${uniq.length}): ${uniq.slice(0, 12).map((u) => `${u.cusip}/${u.issuerName}`).join('; ')}`); } out.push({ id: f.id, name: f.fund_name, cik: f.ci_key, result, hasTarget }); } catch (e) { console.error(`[fund] ${f.fund_name} FAILED:`, e instanceof Error ? e.message : e); out.push({ id: f.id, name: f.fund_name, cik: f.ci_key, result: { error: String(e) }, hasTarget: false }); } } return { funds: out }; } function requeuePortfolio(db: ReturnType): string[] { const rows = db .prepare(`SELECT DISTINCT symbol FROM portfolio_holdings WHERE status='open' ORDER BY symbol`) .all() as Array<{ symbol: string }>; const symbols = rows.map((r) => r.symbol.toUpperCase()); db.prepare("DELETE FROM queue_state WHERE key LIKE 'source_cooldown:%' AND key LIKE '%sec%'").run(); for (const sym of symbols) { queueSecFetch(db, sym); } console.log(`[queue] pending sec-fetch for ${symbols.join(', ')}`); return symbols; } function etDeadlineMs(now = Date.now()): number { const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(new Date(now)); const get = (t: string) => parts.find((p) => p.type === t)?.value ?? ''; const y = get('year'); const m = get('month'); const d = get('day'); return Date.parse(`${y}-${m}-${d}T23:59:00-04:00`); } async function main() { const watch = process.argv.includes('--watch'); const db = createDb(); seedCuratedCusips(db); const first = await syncFunds(db); requeuePortfolio(db); const alpine = first.funds.find((f) => /alpine/i.test(f.name)); if (alpine?.hasTarget) { console.log(`DONE Alpine Fox ${TARGET_QUARTER} is in institution_filings`); return; } if (!watch) { console.log(`${TARGET_QUARTER} not filed yet for Alpine Fox. Re-run with --watch to poll.`); return; } const until = etDeadlineMs(); console.log(`[watch] polling every ${POLL_MS / 60000}m until ${new Date(until).toISOString()}`); while (Date.now() < until) { await new Promise((r) => setTimeout(r, POLL_MS)); const next = await syncFunds(db); const hit = next.funds.find((f) => /alpine/i.test(f.name) && f.hasTarget); if (hit) { requeuePortfolio(db); console.log(`DONE Alpine Fox ${TARGET_QUARTER} ingested`); return; } console.log(`[watch] still no ${TARGET_QUARTER} for Alpine Fox at ${new Date().toISOString()}`); } console.log(`FAILED Alpine Fox ${TARGET_QUARTER} not on EDGAR before deadline`); } main().catch((e) => { console.error(e instanceof Error ? e.message : e); process.exit(1); });