Files

151 lines
5.5 KiB
TypeScript
Raw Permalink Normal View History

// 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);