feat: dealer flow, mirror portfolio (M21), options convexity, FINRA short interest, alert producers, vendor gate
CI / Test & Type-Check (push) Canceled after 0s
CI / Test & Type-Check (push) Canceled after 0s
Snapshot of in-progress module work across multiple slices: - Dealer Flow: dealerExposureEngine, dealerMapService, dealerMapExplain, dealerMapIntegrity, dealerMapReplay, dealerStudyEngine, hanStyleLevels - Mirror Portfolio (M21): fundRepository, captureIngest, mirrorAlertProducers, fund holdings strip, live book, position capture ingest - Options: BSM, NormalizedOptionSurface types, OptionsChainRouter, ConvexityGate, option legs panel - Alert producers: vixLevel, rotation, thesis, unlock, portfolioRisk, mirror (fund_capture, fund_13f, mirror_diff) - FINRA short interest adapter + queue integration - SEC company tickers adapter + ingest (symbol search index seed) - Vendor gate (rate-limit-first data plane, ADR-0009) - CUSIP registry, reverse 13F refresh, stock float service - LRU cache, portfolio backtest engine - Frontend: dealer-flow, funds, journal, lab, monitor, plan, portfolio, reports, screener, strategies, theses, guided-start, exits, more pages - Volume profile, workspace profile, visibility-aware poll - ADRs 0010 (mirror math not advice), 0011 (symbol search index) - VENDOR_INTEGRATIONS.md, END_USER_TEST.md - .gitignore: exclude DBs, .DS_Store, local config, agent scratch
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// One-shot: refresh IREN options surface, rebuild dealer map, diff vs Heatseeker snapshot.
|
||||
import { db as openDb } from '../src/db/client.ts';
|
||||
import { createCacheRepository, type SourceKind } from '../src/cache/CacheRepository.ts';
|
||||
import { AdapterQueue } from '../src/queue/AdapterQueue.ts';
|
||||
import { composeYFinanceWithOptions } from '../src/options/OptionsChainRouter.ts';
|
||||
import { YFinanceAdapter } from '../src/adapters/YFinanceAdapter.ts';
|
||||
import { OptionsAdapter } from '../src/adapters/OptionsAdapter.ts';
|
||||
import type { SourceFetch } from '../src/adapters/SourceAdapter.ts';
|
||||
import { clearVendorRateLimit } from '../src/services/vendorGate.ts';
|
||||
import {
|
||||
getDealerMap,
|
||||
recomputeDealerMapFromCacheDetailed,
|
||||
scheduleDealerMapInputs,
|
||||
} from '../src/analysis/dealerMapService.ts';
|
||||
|
||||
const database = openDb();
|
||||
clearVendorRateLimit('yfinance');
|
||||
for (const k of database
|
||||
.prepare("SELECT key FROM queue_state WHERE key LIKE 'source_cooldown%yfinance%' OR key LIKE 'source_cooldown:yfinance%'")
|
||||
.all() as Array<{ key: string }>) {
|
||||
database.prepare('DELETE FROM queue_state WHERE key=?').run(k.key);
|
||||
}
|
||||
|
||||
const yfinanceAdapter = composeYFinanceWithOptions(
|
||||
new YFinanceAdapter({ db: database }) as unknown as SourceFetch,
|
||||
new OptionsAdapter() as unknown as SourceFetch,
|
||||
);
|
||||
const adapters = new Map<SourceKind, SourceFetch>([['yfinance' as const, yfinanceAdapter]]);
|
||||
const queue = new AdapterQueue({ db: database, adapters, rateLimitMs: { yfinance: 0 } });
|
||||
const cache = createCacheRepository({ db: database, scheduler: queue });
|
||||
queue.cache = cache;
|
||||
const deps = { db: database, cache, queue };
|
||||
|
||||
console.log('1) schedule quote + chains for IREN...');
|
||||
await scheduleDealerMapInputs(deps, 'IREN', 6);
|
||||
|
||||
console.log('2) drain until chains land (max ~120s)...');
|
||||
const t0 = Date.now();
|
||||
let rounds = 0;
|
||||
while (Date.now() - t0 < 120_000) {
|
||||
await queue.drain();
|
||||
rounds += 1;
|
||||
const chains = database
|
||||
.prepare(
|
||||
`SELECT expiry, COUNT(*) n, MAX(ts) ts FROM options_chains
|
||||
WHERE symbol='IREN' AND expiry >= date('now') GROUP BY expiry ORDER BY expiry LIMIT 8`,
|
||||
)
|
||||
.all() as Array<{ expiry: string; n: number; ts: string }>;
|
||||
const quote = database.prepare(`SELECT observed_at FROM kv_cache WHERE key='yfinance:quote:IREN'`).get() as
|
||||
| { observed_at: string }
|
||||
| undefined;
|
||||
if (rounds % 4 === 0) {
|
||||
console.log(
|
||||
' round',
|
||||
rounds,
|
||||
'expiries',
|
||||
chains.map((c) => `${c.expiry}:${c.n}`).join(','),
|
||||
'quote',
|
||||
quote?.observed_at?.slice(0, 19),
|
||||
);
|
||||
}
|
||||
const fresh = chains.filter((c) => c.ts && Date.now() - Date.parse(c.ts) < 45 * 60_000);
|
||||
if (fresh.length >= 4 && quote && Date.now() - Date.parse(quote.observed_at) < 45 * 60_000) break;
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
}
|
||||
|
||||
const detail = recomputeDealerMapFromCacheDetailed(database, 'IREN', { maxExpiries: 6, write: true });
|
||||
console.log('3) recompute', {
|
||||
asOf: detail.map?.asOf,
|
||||
spot: detail.map?.spot,
|
||||
king: detail.map?.levels?.king,
|
||||
keptPrior: detail.keptPriorMap,
|
||||
msg: detail.message,
|
||||
integrity: detail.integrity?.status,
|
||||
});
|
||||
|
||||
const result = await getDealerMap(deps, 'IREN', { forceSchedule: true, maxExpiries: 6 });
|
||||
const m = result.map ?? detail.map;
|
||||
if (!m) {
|
||||
console.error('NO MAP after rebuild');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fmt = (n: number) => {
|
||||
const k = n / 1000;
|
||||
if (Math.abs(k) >= 1000) return `$${(k / 1000).toFixed(1)}M`;
|
||||
return `$${k.toFixed(1)}K`;
|
||||
};
|
||||
|
||||
// Heatseeker LIVE ~03:45:52 PM, IREN 40.41, GEX 0DTE column from screenshot
|
||||
const hs0: Record<number, { v: number; label: string }> = {
|
||||
48: { v: 742_400, label: '+742.4K' },
|
||||
45: { v: 231_800, label: '+231.8K' },
|
||||
43: { v: 284_600, label: '+284.6K' },
|
||||
42: { v: -455_900, label: '-455.9K' },
|
||||
41.5: { v: 288_300, label: '+288.3K' },
|
||||
41: { v: -605_900, label: '-605.9K' },
|
||||
40.5: { v: 3_192_100, label: '+3.19M' },
|
||||
40: { v: -5_735_300, label: '-5.74M ★' },
|
||||
39.5: { v: 2_021_300, label: '+2.02M' },
|
||||
39: { v: -37_200, label: '-37.2K' },
|
||||
38: { v: 1_502_600, label: '+1.50M' },
|
||||
37.5: { v: 567_400, label: '+567K' },
|
||||
37: { v: 638_000, label: '+638K' },
|
||||
34: { v: -1_402_300, label: '-1.40M' },
|
||||
};
|
||||
|
||||
console.log('\n=== LIVE rebuild vs HS 0DTE (HS @40.41 ~3:45pm ET) ===');
|
||||
console.log('OUR spot', m.spot, 'asOf', m.asOf, 'status', result.status);
|
||||
console.log(
|
||||
'strike'.padStart(6),
|
||||
'HS'.padStart(12),
|
||||
'OUR_0DTE'.padStart(12),
|
||||
'ratio'.padStart(8),
|
||||
'sign',
|
||||
);
|
||||
let ok = 0;
|
||||
let flip = 0;
|
||||
for (const s of Object.keys(hs0)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a)) {
|
||||
const cell = m.cells.find((c) => c.strike === s && c.expiry === '2026-08-07');
|
||||
const our = cell?.gex ?? null;
|
||||
const hs = hs0[s];
|
||||
let ratio = '—';
|
||||
let sign = '—';
|
||||
if (our != null && Math.abs(hs.v) > 0) {
|
||||
ratio = (our / hs.v).toFixed(2);
|
||||
if ((our < 0) === (hs.v < 0)) ok += 1;
|
||||
else flip += 1;
|
||||
sign = (our < 0) === (hs.v < 0) ? 'OK' : 'FLIP';
|
||||
}
|
||||
console.log(
|
||||
String(s).padStart(6),
|
||||
hs.label.padStart(12),
|
||||
(our == null ? 'miss' : fmt(our)).padStart(12),
|
||||
ratio.padStart(8),
|
||||
sign,
|
||||
);
|
||||
}
|
||||
console.log(`\nsign match ${ok} / flip ${flip}`);
|
||||
console.log('king', m.levels?.king);
|
||||
console.log('flip/walls', { flip: m.levels?.flip, callWall: m.levels?.callWall, putWall: m.levels?.putWall });
|
||||
console.log('quality', m.quality);
|
||||
const c35 = m.cells.find((c) => c.strike === 35 && c.expiry === '2026-08-21');
|
||||
const c34 = m.cells.find((c) => c.strike === 34 && c.expiry === '2026-08-21');
|
||||
console.log('Aug21 35', c35 ? fmt(c35.gex) : 'miss', 'HS~+3.3M');
|
||||
console.log('Aug21 34', c34 ? fmt(c34.gex) : 'miss', 'HS~+2.9M');
|
||||
console.log('\nDone.');
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user