feat: Unraid deploy, dealer-flow heatmap, confluence zones, 13F capture
CI / Test (push) Canceled after 0s
CI / Build and push (push) Canceled after 0s

Ship Node production images, Unraid compose, and Gitea CI/CD (test then
push registry images; cron script if no runner). Rebuild dealer flow as a
heatmap-first map with integrity gates and chart helpers. Add confluence
zone rules, session clock, capture evidence, and tighter 13F/queue/options
paths, plus the matching UI and tests.
This commit is contained in:
Investor Flow Build
2026-08-18 14:10:02 -04:00
parent f7efea2178
commit 90e1829d39
113 changed files with 7606 additions and 1947 deletions
+127
View File
@@ -0,0 +1,127 @@
#!/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<typeof createDb>, 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<typeof createDb>): 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<typeof createDb>): 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);
});