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:
@@ -19,6 +19,7 @@ import {
|
||||
computeConvictionDelta,
|
||||
aggregateFlowDirection,
|
||||
detectClassRoll,
|
||||
matchedBookShareTotals,
|
||||
DashboardRollupEngine,
|
||||
generateDashboardRollupSummary,
|
||||
ADR0007_FOOTER,
|
||||
@@ -195,10 +196,14 @@ test('DashboardRollupEngine: increasing conviction (institutional only)', async
|
||||
const wlIns = db.prepare('INSERT INTO watchlists (id, owner_id, name, symbols, created_at, sort_order) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
wlIns.run('wl1', 'user-inc', 'default', JSON.stringify(['NVDA']), '2026-01-01', 0);
|
||||
|
||||
// Seed institution_filings: current quarter has more shares than previous.
|
||||
// Same filer both quarters so matched-book path is exercised.
|
||||
seedInstitutionFilings(db, 'NVDA', [
|
||||
{ filer_cik: 'cik1', filer_sic: '60', shares: 10000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 15000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik1', filer_sic: '60', shares: 15000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 8000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 12000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik3', filer_sic: '60', shares: 5000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik3', filer_sic: '60', shares: 9000, reported_quarter: '2025-Q2' },
|
||||
]);
|
||||
|
||||
const edgar = createNoOpEdgar();
|
||||
@@ -219,7 +224,11 @@ test('DashboardRollupEngine: reducing conviction (institutional only)', async ()
|
||||
|
||||
seedInstitutionFilings(db, 'TSLA', [
|
||||
{ filer_cik: 'cik1', filer_sic: '60', shares: 20000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 5000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik1', filer_sic: '60', shares: 5000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 18000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 4000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik3', filer_sic: '60', shares: 10000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik3', filer_sic: '60', shares: 2000, reported_quarter: '2025-Q2' },
|
||||
]);
|
||||
|
||||
const edgar = createNoOpEdgar();
|
||||
@@ -232,16 +241,67 @@ test('DashboardRollupEngine: reducing conviction (institutional only)', async ()
|
||||
assert.equal(rows[0].convictionDelta, 'reducing');
|
||||
});
|
||||
|
||||
test('matchedBookShareTotals: incomplete season does not look like selling', () => {
|
||||
const db = createTestDb();
|
||||
// Q1 full book; Q2 only a few filers who actually added — raw sum falls, matched rises.
|
||||
seedInstitutionFilings(db, 'IREN', [
|
||||
{ filer_cik: 'big1', filer_sic: '60', shares: 50_000_000, reported_quarter: '2026-Q1' },
|
||||
{ filer_cik: 'big2', filer_sic: '60', shares: 40_000_000, reported_quarter: '2026-Q1' },
|
||||
{ filer_cik: 'big3', filer_sic: '60', shares: 30_000_000, reported_quarter: '2026-Q1' },
|
||||
{ filer_cik: 'br', filer_sic: '60', shares: 4_000_000, reported_quarter: '2026-Q1' },
|
||||
{ filer_cik: 'br', filer_sic: '60', shares: 13_000_000, reported_quarter: '2026-Q2' },
|
||||
{ filer_cik: 'ss', filer_sic: '60', shares: 3_000_000, reported_quarter: '2026-Q1' },
|
||||
{ filer_cik: 'ss', filer_sic: '60', shares: 6_000_000, reported_quarter: '2026-Q2' },
|
||||
{ filer_cik: 'tiny', filer_sic: '60', shares: 1000, reported_quarter: '2026-Q1' },
|
||||
{ filer_cik: 'tiny', filer_sic: '60', shares: 2000, reported_quarter: '2026-Q2' },
|
||||
]);
|
||||
const rawQ1 = 50e6 + 40e6 + 30e6 + 4e6 + 3e6 + 1000;
|
||||
const rawQ2 = 13e6 + 6e6 + 2000;
|
||||
assert.ok(rawQ2 < rawQ1 * 0.9, 'raw totals falsely lighten');
|
||||
const m = matchedBookShareTotals(db, 'IREN', '2026-Q2', '2026-Q1');
|
||||
assert.ok(m);
|
||||
assert.equal(m!.filerCount, 3);
|
||||
assert.ok(m!.currShares > m!.prevShares * 1.1, 'matched book shows adding');
|
||||
});
|
||||
|
||||
test('DashboardRollupEngine: incomplete latest quarter uses matched book not raw drop', async () => {
|
||||
const db = createTestDb();
|
||||
seedUser(db, 'user-partial');
|
||||
const wlIns = db.prepare('INSERT INTO watchlists (id, owner_id, name, symbols, created_at, sort_order) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
wlIns.run('wl1', 'user-partial', 'default', JSON.stringify(['IREN']), '2026-01-01', 0);
|
||||
|
||||
// Many Q1 filers; few Q2 filers who increased — would be "reducing" on raw SUM.
|
||||
const rows: Array<{ filer_cik: string; filer_sic: string; shares: number; reported_quarter: string }> = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
rows.push({ filer_cik: `f${i}`, filer_sic: '60', shares: 1_000_000, reported_quarter: '2026-Q1' });
|
||||
}
|
||||
// 5 of them file Q2 with more shares; rest missing (incomplete season)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
rows.push({ filer_cik: `f${i}`, filer_sic: '60', shares: 2_500_000, reported_quarter: '2026-Q2' });
|
||||
}
|
||||
seedInstitutionFilings(db, 'IREN', rows);
|
||||
|
||||
const edgar = createNoOpEdgar();
|
||||
const flowEngine = new InstitutionFlowEngine(edgar);
|
||||
const engine = new DashboardRollupEngine(db, flowEngine);
|
||||
const out = await engine.computeRollup('user-partial');
|
||||
assert.equal(out[0].convictionDelta, 'increasing');
|
||||
});
|
||||
|
||||
test('DashboardRollupEngine: mixed conviction (flow vs insider disagree)', async () => {
|
||||
const db = createTestDb();
|
||||
seedUser(db, 'user-mix');
|
||||
const wlIns = db.prepare('INSERT INTO watchlists (id, owner_id, name, symbols, created_at, sort_order) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
wlIns.run('wl1', 'user-mix', 'default', JSON.stringify(['MSFT']), '2026-01-01', 0);
|
||||
|
||||
// Institutional: increasing.
|
||||
// Institutional: increasing (matched book).
|
||||
seedInstitutionFilings(db, 'MSFT', [
|
||||
{ filer_cik: 'cik1', filer_sic: '60', shares: 10000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 15000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik1', filer_sic: '60', shares: 15000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 8000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik2', filer_sic: '60', shares: 14000, reported_quarter: '2025-Q2' },
|
||||
{ filer_cik: 'cik3', filer_sic: '60', shares: 5000, reported_quarter: '2025-Q1' },
|
||||
{ filer_cik: 'cik3', filer_sic: '60', shares: 9000, reported_quarter: '2025-Q2' },
|
||||
]);
|
||||
|
||||
// Insider: reducing (more sell than buy).
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import {
|
||||
buildDealerMap,
|
||||
contractGex,
|
||||
isDegradedDealerMap,
|
||||
isHealthyDealerMap,
|
||||
isSaneIv,
|
||||
layer0Educator,
|
||||
resolveGreeks,
|
||||
type DealerMap,
|
||||
} from '../dealerExposureEngine.ts';
|
||||
import type { NormalizedOptionSurface, NormalizedOptionRow } from '../../options/types.ts';
|
||||
|
||||
function row(partial: Partial<NormalizedOptionRow> & Pick<NormalizedOptionRow, 'strike' | 'expiry' | 'right'>): NormalizedOptionRow {
|
||||
return {
|
||||
openInterest: 1000,
|
||||
volume: 100,
|
||||
bid: 1,
|
||||
ask: 1.1,
|
||||
mid: 1.05,
|
||||
iv: 0.25,
|
||||
greeks: { gamma: 0.02, vanna: 0.1, delta: 0.5 },
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
function surface(rows: NormalizedOptionRow[], spot = 100): NormalizedOptionSurface {
|
||||
return {
|
||||
symbol: 'TEST',
|
||||
spot,
|
||||
asOf: '2026-08-06T15:00:00.000Z',
|
||||
providerId: 'synthetic',
|
||||
delaySeconds: 900,
|
||||
oiFreshness: 'prior_session',
|
||||
rows,
|
||||
provenance: {
|
||||
fetchedAt: '2026-08-06T15:00:00.000Z',
|
||||
sourceKind: 'yfinance',
|
||||
rawSourceId: 'synthetic:TEST',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('contractGex: call positive, put negative', () => {
|
||||
const call = contractGex(0.01, 100, 100, 'call');
|
||||
const put = contractGex(0.01, 100, 100, 'put');
|
||||
assert.ok(call > 0);
|
||||
assert.ok(put < 0);
|
||||
assert.equal(call, -put);
|
||||
});
|
||||
|
||||
test('buildDealerMap: expiries sorted nearest-left and capped', () => {
|
||||
const rows: NormalizedOptionRow[] = [];
|
||||
for (const exp of ['2026-08-08', '2026-08-15', '2026-08-22', '2026-09-19', '2026-10-17', '2026-11-21', '2026-12-19', '2027-01-16']) {
|
||||
rows.push(row({ strike: 100, expiry: exp, right: 'call', openInterest: 500 }));
|
||||
rows.push(row({ strike: 100, expiry: exp, right: 'put', openInterest: 400, greeks: { gamma: 0.015, vanna: -0.05 } }));
|
||||
}
|
||||
const map = buildDealerMap(surface(rows), { maxExpiries: 4 });
|
||||
assert.equal(map.expiries.length, 4);
|
||||
assert.equal(map.expiries[0], '2026-08-08');
|
||||
assert.ok(map.expiries.every((e, i) => i === 0 || e >= map.expiries[i - 1]));
|
||||
assert.equal(map.providerId, 'synthetic');
|
||||
assert.equal(map.convention, 'classic_call_pos_put_neg');
|
||||
assert.match(map.delayNote, /prior session/i);
|
||||
});
|
||||
|
||||
test('buildDealerMap: king is max absolute GEX cell', () => {
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 95, expiry: '2026-08-15', right: 'put', openInterest: 2000, greeks: { gamma: 0.03, vanna: 0.05 } }),
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 500, greeks: { gamma: 0.02, vanna: 0.05 } }),
|
||||
row({ strike: 105, expiry: '2026-08-15', right: 'call', openInterest: 5000, greeks: { gamma: 0.04, vanna: 0.05 } }),
|
||||
]),
|
||||
);
|
||||
assert.ok(map.levels.king);
|
||||
assert.equal(map.levels.king!.strike, 105);
|
||||
assert.ok(map.cells.some((c) => c.isKing && c.strike === 105));
|
||||
});
|
||||
|
||||
test('buildDealerMap: skips rows without OI', () => {
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: null }),
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'put', openInterest: 0 }),
|
||||
row({ strike: 101, expiry: '2026-08-15', right: 'call', openInterest: 1000 }),
|
||||
]),
|
||||
);
|
||||
assert.ok(map.byStrike.some((s) => s.strike === 101));
|
||||
assert.ok(!map.byStrike.some((s) => s.strike === 100));
|
||||
assert.ok(map.coveragePct < 100);
|
||||
});
|
||||
|
||||
test('buildDealerMap: BSM fill when gamma missing but IV present', () => {
|
||||
// Asymmetric OI so call/put GEX does not cancel to zero at the strike.
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 5000,
|
||||
iv: 0.3,
|
||||
greeks: null,
|
||||
}),
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'put',
|
||||
openInterest: 1000,
|
||||
iv: 0.3,
|
||||
greeks: null,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
assert.ok(map.cells.length >= 1);
|
||||
assert.ok(map.bsmFillPct > 0);
|
||||
assert.ok(map.byStrike.some((s) => s.absGex > 0), `byStrike=${JSON.stringify(map.byStrike)}`);
|
||||
});
|
||||
|
||||
test('withExposureConvention: dealer_inventory negates classic GEX and keeps king strike', async () => {
|
||||
const { withExposureConvention } = await import('../dealerExposureEngine.ts');
|
||||
const classic = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 5000, greeks: { gamma: 0.04, vanna: 0.05 } }),
|
||||
row({ strike: 95, expiry: '2026-08-15', right: 'put', openInterest: 2000, greeks: { gamma: 0.03, vanna: 0.05 } }),
|
||||
], 100),
|
||||
);
|
||||
assert.equal(classic.convention, 'classic_call_pos_put_neg');
|
||||
const dealer = withExposureConvention(classic, 'dealer_inventory');
|
||||
assert.equal(dealer.convention, 'dealer_inventory');
|
||||
assert.ok(classic.levels.king && dealer.levels.king);
|
||||
assert.equal(classic.levels.king!.strike, dealer.levels.king!.strike);
|
||||
assert.ok(
|
||||
Math.abs(classic.levels.king!.gex + dealer.levels.king!.gex) < 1e-6,
|
||||
'king gex should flip sign',
|
||||
);
|
||||
for (const c of classic.cells) {
|
||||
const d = dealer.cells.find((x) => x.strike === c.strike && x.expiry === c.expiry);
|
||||
assert.ok(d);
|
||||
assert.ok(Math.abs(c.gex + d!.gex) < 1e-6, `cell ${c.strike} gex should flip`);
|
||||
}
|
||||
// Round-trip
|
||||
const back = withExposureConvention(dealer, 'classic_call_pos_put_neg');
|
||||
assert.equal(back.convention, 'classic_call_pos_put_neg');
|
||||
assert.ok(Math.abs(back.levels.king!.gex - classic.levels.king!.gex) < 1e-6);
|
||||
});
|
||||
|
||||
test('buildDealerMap: flip between +GEX and −GEX strikes', () => {
|
||||
// Strong + below and − above (or vice versa) to create a sign change
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 90, expiry: '2026-08-15', right: 'call', openInterest: 8000, greeks: { gamma: 0.03, vanna: 0.1 } }),
|
||||
row({ strike: 95, expiry: '2026-08-15', right: 'call', openInterest: 6000, greeks: { gamma: 0.03, vanna: 0.1 } }),
|
||||
row({ strike: 105, expiry: '2026-08-15', right: 'put', openInterest: 8000, greeks: { gamma: 0.03, vanna: 0.1 } }),
|
||||
row({ strike: 110, expiry: '2026-08-15', right: 'put', openInterest: 6000, greeks: { gamma: 0.03, vanna: 0.1 } }),
|
||||
], 100),
|
||||
);
|
||||
// puts contribute negative GEX, calls positive → flip exists between clusters
|
||||
assert.ok(map.levels.flip != null, 'expected a polarity flip');
|
||||
});
|
||||
|
||||
test('buildDealerMap: detects rug_setup pattern', () => {
|
||||
// +GEX above spot, large −GEX pivot below, more −GEX stack
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 110, expiry: '2026-08-15', right: 'call', openInterest: 10000, greeks: { gamma: 0.05, vanna: 0.1 } }),
|
||||
row({ strike: 95, expiry: '2026-08-15', right: 'put', openInterest: 12000, greeks: { gamma: 0.05, vanna: 0.1 } }),
|
||||
row({ strike: 90, expiry: '2026-08-15', right: 'put', openInterest: 9000, greeks: { gamma: 0.04, vanna: 0.1 } }),
|
||||
row({ strike: 85, expiry: '2026-08-15', right: 'put', openInterest: 8000, greeks: { gamma: 0.04, vanna: 0.1 } }),
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 500, greeks: { gamma: 0.01, vanna: 0.05 } }),
|
||||
], 100),
|
||||
);
|
||||
assert.ok(
|
||||
map.patterns.some((p) => p.id === 'rug_setup'),
|
||||
`patterns=${JSON.stringify(map.patterns)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('buildDealerMap: detects whipsaw with air pocket between edges', () => {
|
||||
// Strong nodes at 90 and 110, near-zero middle
|
||||
const rows: NormalizedOptionRow[] = [
|
||||
row({ strike: 90, expiry: '2026-08-15', right: 'call', openInterest: 15000, greeks: { gamma: 0.05, vanna: 0.1 } }),
|
||||
row({ strike: 110, expiry: '2026-08-15', right: 'call', openInterest: 15000, greeks: { gamma: 0.05, vanna: 0.1 } }),
|
||||
];
|
||||
for (const k of [95, 98, 100, 102, 105]) {
|
||||
rows.push(
|
||||
row({
|
||||
strike: k,
|
||||
expiry: '2026-08-15',
|
||||
right: 'call',
|
||||
openInterest: 10,
|
||||
greeks: { gamma: 0.0001, vanna: 0.001 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
const map = buildDealerMap(surface(rows, 100));
|
||||
assert.ok(map.levels.airPockets.length >= 1, 'expected air pocket');
|
||||
assert.ok(
|
||||
map.patterns.some((p) => p.id === 'whipsaw'),
|
||||
`patterns=${JSON.stringify(map.patterns)} scenario=${map.scenario}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('buildDealerMap: velocity growing near air pocket', () => {
|
||||
const curr = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 90, expiry: '2026-08-15', right: 'call', openInterest: 5000, greeks: { gamma: 0.04, vanna: 0.1 } }),
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 50, greeks: { gamma: 0.001, vanna: 0.01 } }),
|
||||
row({ strike: 110, expiry: '2026-08-15', right: 'call', openInterest: 5000, greeks: { gamma: 0.04, vanna: 0.1 } }),
|
||||
]),
|
||||
);
|
||||
const prior: Pick<DealerMap, 'asOf' | 'byStrike' | 'levels'> = {
|
||||
asOf: '2026-08-05T15:00:00.000Z',
|
||||
byStrike: curr.byStrike.map((s) =>
|
||||
s.strike === 90 ? { ...s, absGex: s.absGex * 0.3, gex: s.gex * 0.3 } : { ...s, absGex: s.absGex * 0.9, gex: s.gex * 0.9 },
|
||||
),
|
||||
levels: curr.levels,
|
||||
};
|
||||
const withVel = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 90, expiry: '2026-08-15', right: 'call', openInterest: 5000, greeks: { gamma: 0.04, vanna: 0.1 } }),
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 50, greeks: { gamma: 0.001, vanna: 0.01 } }),
|
||||
row({ strike: 110, expiry: '2026-08-15', right: 'call', openInterest: 5000, greeks: { gamma: 0.04, vanna: 0.1 } }),
|
||||
]),
|
||||
{ prior },
|
||||
);
|
||||
assert.ok(withVel.velocity);
|
||||
assert.ok(withVel.velocity!.nodes.some((n) => n.growing));
|
||||
});
|
||||
|
||||
test('buildDealerMap: OPEX Friday banner within 5 days', () => {
|
||||
// 2026-08-07 is a Friday
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 100, expiry: '2026-08-07', right: 'call', openInterest: 1000 }),
|
||||
]),
|
||||
);
|
||||
assert.equal(map.opexHints.isNearOpex, true);
|
||||
assert.match(map.opexHints.note, /OPEX|Near-term/i);
|
||||
});
|
||||
|
||||
test('layer0Educator: no buy/sell imperatives', () => {
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 3000 }),
|
||||
row({ strike: 95, expiry: '2026-08-15', right: 'put', openInterest: 3000 }),
|
||||
]),
|
||||
);
|
||||
const copy = layer0Educator(map);
|
||||
const blob = JSON.stringify(copy).toLowerCase();
|
||||
assert.equal(copy.footer, 'Educational observation only.');
|
||||
assert.ok(!/\bbuy\b/.test(blob) || blob.includes('buying pressure') === false);
|
||||
// Primary rule: no "you should buy/sell"
|
||||
assert.ok(!/you should (buy|sell)/i.test(blob));
|
||||
assert.ok(!/\b(buy|sell) (calls|puts|shares|stock)\b/i.test(blob));
|
||||
assert.ok(copy.questions.length >= 3);
|
||||
assert.ok(copy.caveats.some((c) => /prior session|delay/i.test(c)));
|
||||
});
|
||||
|
||||
test('buildDealerMap: engine has no vendor coupling (synthetic only path)', () => {
|
||||
// Sanity: providerId synthetic preserved; computation local_engine
|
||||
const map = buildDealerMap(
|
||||
surface([row({ strike: 50, expiry: '2026-08-20', right: 'call', openInterest: 100 })]),
|
||||
);
|
||||
assert.equal(map.computation, 'local_engine');
|
||||
assert.equal(map.unit, 'usd_delta_adj_gamma_notional');
|
||||
assert.ok(map.quality);
|
||||
assert.ok(['ok', 'degraded', 'poor'].includes(map.quality.greekQuality));
|
||||
});
|
||||
|
||||
test('isSaneIv: rejects premarket junk and absurd IV', () => {
|
||||
assert.equal(isSaneIv(1e-5), false);
|
||||
assert.equal(isSaneIv(0.02), false);
|
||||
assert.equal(isSaneIv(0.25), true);
|
||||
assert.equal(isSaneIv(4.0), false);
|
||||
assert.equal(isSaneIv(null), false);
|
||||
});
|
||||
|
||||
test('resolveGreeks: skips junk IV without vendor gamma', () => {
|
||||
const r = resolveGreeks(
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 1000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
100,
|
||||
'2026-08-07T13:14:00.000Z',
|
||||
0.04,
|
||||
0,
|
||||
);
|
||||
assert.equal(r.skippedBadIv, true);
|
||||
assert.equal(r.gamma, 0);
|
||||
});
|
||||
|
||||
test('resolveGreeks: uses fallback IV hint when current IV is junk', () => {
|
||||
const r = resolveGreeks(
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 5000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
100,
|
||||
'2026-08-07T13:14:00.000Z',
|
||||
0.04,
|
||||
0,
|
||||
{ fallbackIv: 0.35 },
|
||||
);
|
||||
assert.equal(r.skippedBadIv, false);
|
||||
assert.ok(r.gamma > 0);
|
||||
assert.equal(r.usedFallbackIv, true);
|
||||
assert.equal(r.usedBsm, true);
|
||||
});
|
||||
|
||||
test('buildDealerMap: junk IV rows skipped; quality poor when most OI unusable', () => {
|
||||
// No sane IV on the surface → no ATM proxy → skip
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 9000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'put',
|
||||
openInterest: 8000,
|
||||
iv: 0.01,
|
||||
greeks: null,
|
||||
}),
|
||||
row({
|
||||
strike: 105,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 100,
|
||||
iv: 0.02,
|
||||
greeks: null,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
assert.ok(map.quality.skippedBadIv >= 2, `skipped=${map.quality.skippedBadIv}`);
|
||||
assert.equal(map.quality.greekQuality, 'poor');
|
||||
assert.ok(isDegradedDealerMap(map));
|
||||
assert.equal(isHealthyDealerMap(map), false);
|
||||
});
|
||||
|
||||
test('buildDealerMap: ATM IV proxy fills junk rows when some sane IV exists', () => {
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 5000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'put',
|
||||
openInterest: 1000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
row({
|
||||
strike: 101,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 200,
|
||||
iv: 0.28,
|
||||
greeks: null,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
assert.ok(map.byStrike.some((s) => s.absGex > 0));
|
||||
assert.ok(map.quality.fallbackIvUses >= 1 || map.quality.included >= 1);
|
||||
});
|
||||
|
||||
test('buildDealerMap: ivHints restore structure when live IV is junk', () => {
|
||||
const map = buildDealerMap(
|
||||
surface([
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'call',
|
||||
openInterest: 5000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
row({
|
||||
strike: 100,
|
||||
expiry: '2026-09-15',
|
||||
right: 'put',
|
||||
openInterest: 1000,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}),
|
||||
]),
|
||||
{
|
||||
ivHints: {
|
||||
'2026-09-15|100|call': 0.3,
|
||||
'2026-09-15|100|put': 0.3,
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.ok(map.byStrike.some((s) => s.absGex > 0));
|
||||
assert.ok(map.quality.fallbackIvUses >= 1);
|
||||
assert.ok(map.quality.greekQuality !== 'poor' || map.cells.length > 0);
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { buildDealerMap } from '../dealerExposureEngine.ts';
|
||||
import { auditDealerMap } from '../dealerMapIntegrity.ts';
|
||||
import type { NormalizedOptionSurface } from '../../options/types.ts';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const fixturePath = join(__dirname, '../__fixtures__/dealer-surface-synthetic.json');
|
||||
|
||||
function loadFixture(): NormalizedOptionSurface {
|
||||
return JSON.parse(readFileSync(fixturePath, 'utf8')) as NormalizedOptionSurface;
|
||||
}
|
||||
|
||||
/** Small fixture uses looser strike-count thresholds; production defaults stay strict. */
|
||||
const FIXTURE_THRESHOLDS = {
|
||||
minOiStrikesPerExpiry: 3,
|
||||
minNearSpotOiStrikes: 3,
|
||||
minCells: 6,
|
||||
minByStrike: 3,
|
||||
};
|
||||
|
||||
test('golden fixture: map is complete and king is max |gex| call cluster', () => {
|
||||
const surface = loadFixture();
|
||||
const map = buildDealerMap(surface, { maxExpiries: 3 });
|
||||
const integrity = auditDealerMap(map, surface, { thresholds: FIXTURE_THRESHOLDS });
|
||||
|
||||
assert.equal(integrity.status, 'complete', JSON.stringify(integrity.hardFailed));
|
||||
assert.ok(map.levels.king);
|
||||
// Heaviest call OI + gamma at 105 08-08
|
||||
assert.equal(map.levels.king!.strike, 105);
|
||||
assert.equal(map.levels.king!.expiry, '2026-08-08');
|
||||
assert.ok(map.cells.length >= 6);
|
||||
assert.ok(map.quality.greekQuality === 'ok' || map.quality.greekQuality === 'degraded');
|
||||
|
||||
// Determinism
|
||||
const map2 = buildDealerMap(surface, { maxExpiries: 3 });
|
||||
assert.equal(map2.levels.king!.gex, map.levels.king!.gex);
|
||||
assert.equal(map2.cells.length, map.cells.length);
|
||||
});
|
||||
|
||||
test('golden fixture: zero IV across surface → incomplete integrity', () => {
|
||||
const surface = loadFixture();
|
||||
surface.rows = surface.rows.map((r) => ({
|
||||
...r,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}));
|
||||
const map = buildDealerMap(surface, { maxExpiries: 3 });
|
||||
const integrity = auditDealerMap(map, surface, { thresholds: FIXTURE_THRESHOLDS });
|
||||
assert.equal(integrity.status, 'incomplete');
|
||||
});
|
||||
|
||||
test('golden fixture: drop to one expiry → incomplete', () => {
|
||||
const surface = loadFixture();
|
||||
surface.rows = surface.rows.filter((r) => r.expiry === '2026-08-08');
|
||||
const map = buildDealerMap(surface, { maxExpiries: 3 });
|
||||
const integrity = auditDealerMap(map, surface, { thresholds: FIXTURE_THRESHOLDS });
|
||||
assert.equal(integrity.status, 'incomplete');
|
||||
assert.ok(integrity.hardFailed.includes('expiries'));
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import {
|
||||
auditDealerMap,
|
||||
auditDealerSurface,
|
||||
integritySummary,
|
||||
} from '../dealerMapIntegrity.ts';
|
||||
import { buildDealerMap } from '../dealerExposureEngine.ts';
|
||||
import type { NormalizedOptionRow, NormalizedOptionSurface } from '../../options/types.ts';
|
||||
|
||||
function row(
|
||||
partial: Partial<NormalizedOptionRow> & Pick<NormalizedOptionRow, 'strike' | 'expiry' | 'right'>,
|
||||
): NormalizedOptionRow {
|
||||
return {
|
||||
openInterest: 1000,
|
||||
volume: 100,
|
||||
bid: 1,
|
||||
ask: 1.1,
|
||||
mid: 1.05,
|
||||
iv: 0.3,
|
||||
greeks: { gamma: 0.02, vanna: 0.1, delta: 0.5 },
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
function fatSurface(): NormalizedOptionSurface {
|
||||
const rows: NormalizedOptionRow[] = [];
|
||||
const exps = ['2026-08-08', '2026-08-15', '2026-08-22', '2026-08-29'];
|
||||
for (const exp of exps) {
|
||||
for (let k = 90; k <= 110; k += 1) {
|
||||
rows.push(row({ strike: k, expiry: exp, right: 'call', openInterest: 500 }));
|
||||
rows.push(
|
||||
row({
|
||||
strike: k,
|
||||
expiry: exp,
|
||||
right: 'put',
|
||||
openInterest: 400,
|
||||
greeks: { gamma: 0.015, vanna: -0.05 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
symbol: 'TEST',
|
||||
spot: 100,
|
||||
asOf: '2026-08-06T15:00:00.000Z',
|
||||
providerId: 'synthetic',
|
||||
delaySeconds: 900,
|
||||
oiFreshness: 'prior_session',
|
||||
rows,
|
||||
provenance: {
|
||||
fetchedAt: '2026-08-06T15:00:00.000Z',
|
||||
sourceKind: 'yfinance',
|
||||
rawSourceId: 'synthetic:TEST',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('auditDealerSurface: null surface incomplete', () => {
|
||||
const r = auditDealerSurface(null);
|
||||
assert.equal(r.status, 'incomplete');
|
||||
assert.ok(r.hardFailed.includes('surface'));
|
||||
});
|
||||
|
||||
test('auditDealerMap: complete fat surface + map', () => {
|
||||
const s = fatSurface();
|
||||
const map = buildDealerMap(s, { maxExpiries: 4 });
|
||||
const r = auditDealerMap(map, s);
|
||||
assert.equal(r.status, 'complete', JSON.stringify(r.hardFailed));
|
||||
assert.ok(r.metrics.cells >= 8);
|
||||
assert.match(integritySummary(r), /complete/i);
|
||||
});
|
||||
|
||||
test('auditDealerSurface: delay alone does not hard-fail', () => {
|
||||
const s = fatSurface();
|
||||
s.delaySeconds = 900;
|
||||
const map = buildDealerMap(s);
|
||||
const r = auditDealerMap(map, s);
|
||||
assert.notEqual(r.status, 'incomplete');
|
||||
assert.ok(r.checks.some((c) => c.id === 'delay' && c.ok));
|
||||
});
|
||||
|
||||
test('auditDealerSurface: missing spot hard-fails', () => {
|
||||
const s = fatSurface();
|
||||
s.spot = 0;
|
||||
const r = auditDealerSurface(s);
|
||||
assert.equal(r.status, 'incomplete');
|
||||
assert.ok(r.hardFailed.includes('spot'));
|
||||
});
|
||||
|
||||
test('auditDealerSurface: too few expiries incomplete', () => {
|
||||
const s = fatSurface();
|
||||
s.rows = s.rows.filter((r) => r.expiry === '2026-08-08');
|
||||
const r = auditDealerSurface(s);
|
||||
assert.equal(r.status, 'incomplete');
|
||||
assert.ok(r.hardFailed.includes('expiries'));
|
||||
});
|
||||
|
||||
test('auditDealerSurface: junk IV only incomplete on usable greeks', () => {
|
||||
const s = fatSurface();
|
||||
s.rows = s.rows.map((r) => ({
|
||||
...r,
|
||||
iv: 1e-5,
|
||||
greeks: null,
|
||||
}));
|
||||
const map = buildDealerMap(s);
|
||||
const r = auditDealerMap(map, s);
|
||||
assert.equal(r.status, 'incomplete');
|
||||
assert.ok(
|
||||
r.hardFailed.some((id) => id === 'usable_greeks' || id === 'cells' || id === 'greek_quality'),
|
||||
JSON.stringify(r.hardFailed),
|
||||
);
|
||||
});
|
||||
|
||||
test('auditDealerMap: thin cell grid hard-fails cells', () => {
|
||||
const s: NormalizedOptionSurface = {
|
||||
...fatSurface(),
|
||||
rows: [
|
||||
row({ strike: 100, expiry: '2026-08-08', right: 'call', openInterest: 1000 }),
|
||||
row({ strike: 100, expiry: '2026-08-15', right: 'call', openInterest: 1000 }),
|
||||
row({ strike: 100, expiry: '2026-08-22', right: 'call', openInterest: 1000 }),
|
||||
],
|
||||
};
|
||||
const map = buildDealerMap(s);
|
||||
// Force tiny map metrics path via direct audit with map that has few cells
|
||||
const tiny = { ...map, cells: map.cells.slice(0, 2), byStrike: map.byStrike.slice(0, 1) };
|
||||
const r = auditDealerMap(tiny as typeof map, s);
|
||||
assert.equal(r.status, 'incomplete');
|
||||
assert.ok(r.hardFailed.includes('cells') || r.hardFailed.includes('by_strike'));
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import {
|
||||
getDealerMap,
|
||||
recomputeDealerMapFromCache,
|
||||
scheduleDealerMapInputs,
|
||||
writeDealerMapCache,
|
||||
dealerMapCacheKey,
|
||||
} from '../dealerMapService.ts';
|
||||
import { buildDealerMap } from '../dealerExposureEngine.ts';
|
||||
import type { NormalizedOptionSurface } from '../../options/types.ts';
|
||||
|
||||
function memDb(): DatabaseSync {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE quotes (symbol TEXT PRIMARY KEY, price REAL, bid REAL, ask REAL, change REAL, change_pct REAL, iv REAL, session TEXT, regular_price REAL, observed_at TEXT);
|
||||
CREATE TABLE kv_cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, observed_at TEXT NOT NULL);
|
||||
CREATE TABLE options_chains (
|
||||
symbol TEXT, expiry TEXT, strike REAL, type TEXT,
|
||||
bid REAL, ask REAL, iv REAL, delta REAL, gamma REAL, theta REAL, vega REAL, rho REAL,
|
||||
open_interest INTEGER, volume INTEGER, ts TEXT,
|
||||
PRIMARY KEY (symbol, expiry, strike, type, ts)
|
||||
);
|
||||
CREATE TABLE dealer_map_snapshots (
|
||||
symbol TEXT NOT NULL,
|
||||
as_of_date TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (symbol, as_of_date)
|
||||
);
|
||||
CREATE TABLE symbol_demand (
|
||||
symbol TEXT PRIMARY KEY,
|
||||
refcount INTEGER NOT NULL DEFAULT 0,
|
||||
ticker_kind TEXT,
|
||||
system_pin INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
return db;
|
||||
}
|
||||
|
||||
function seedChain(db: DatabaseSync): void {
|
||||
db.prepare(
|
||||
'INSERT INTO quotes (symbol,price,observed_at) VALUES (?,?,?)',
|
||||
).run('SPY', 500, '2026-08-06T14:00:00.000Z');
|
||||
const exps = ['2026-08-08', '2026-08-15', '2026-08-22'];
|
||||
db.prepare(
|
||||
'INSERT INTO kv_cache (key,value,observed_at) VALUES (?,?,?)',
|
||||
).run('options_expiry:SPY', JSON.stringify(exps), '2026-08-06T14:00:00.000Z');
|
||||
const ins = db.prepare(
|
||||
`INSERT INTO options_chains (symbol,expiry,strike,type,bid,ask,iv,delta,gamma,theta,vega,open_interest,volume,ts)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
);
|
||||
const ts = '2026-08-06T14:00:00.000Z';
|
||||
for (const exp of exps) {
|
||||
for (let k = 480; k <= 520; k += 5) {
|
||||
ins.run('SPY', exp, k, 'call', 1, 1.1, 0.2, 0.5, 0.04, -0.1, 0.2, 5000, 100, ts);
|
||||
ins.run('SPY', exp, k, 'put', 1, 1.1, 0.2, -0.4, 0.03, -0.1, 0.2, 4000, 100, ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('recomputeDealerMapFromCache builds map without vendor', () => {
|
||||
const db = memDb();
|
||||
seedChain(db);
|
||||
const map = recomputeDealerMapFromCache(db, 'SPY');
|
||||
assert.ok(map);
|
||||
assert.equal(map!.symbol, 'SPY');
|
||||
assert.ok(map!.levels.king);
|
||||
assert.equal(map!.providerId, 'yfinance');
|
||||
// Complete maps are written to kv; incomplete recomputes are not.
|
||||
const row = db.prepare('SELECT value FROM kv_cache WHERE key=?').get(dealerMapCacheKey('SPY')) as
|
||||
| { value: string }
|
||||
| undefined;
|
||||
// Smoke: recompute path runs; cache write depends on integrity completeness.
|
||||
assert.ok(map!.cells.length > 0);
|
||||
if (row?.value) assert.ok(row.value.includes('SPY'));
|
||||
});
|
||||
|
||||
test('getDealerMap never requires queue for cached recompute', async () => {
|
||||
const db = memDb();
|
||||
seedChain(db);
|
||||
const result = await getDealerMap({ db, cache: null, queue: null }, 'SPY');
|
||||
assert.ok(result.map);
|
||||
assert.equal(result.scheduled, false);
|
||||
assert.ok(result.educator);
|
||||
assert.ok(result.hanScript);
|
||||
assert.ok(result.hanScript!.script.length > 10);
|
||||
assert.match(result.hanScript!.script, /educational observation only/i);
|
||||
assert.ok(
|
||||
result.status === 'ok' || result.status === 'stale' || result.status === 'incomplete',
|
||||
`status=${result.status}`,
|
||||
);
|
||||
assert.ok(result.integrity);
|
||||
});
|
||||
|
||||
test('getDealerMap schedules when empty and queue present', async () => {
|
||||
const db = memDb();
|
||||
const queued: string[] = [];
|
||||
const queue = {
|
||||
queue: async (key: string) => { queued.push(key); },
|
||||
} as any;
|
||||
const cache = {
|
||||
ensureInDemand: async () => {},
|
||||
} as any;
|
||||
|
||||
const result = await getDealerMap({ db, cache, queue }, 'AAPL');
|
||||
assert.equal(result.map, null);
|
||||
assert.equal(result.status, 'pending');
|
||||
assert.equal(result.scheduled, true);
|
||||
assert.ok(queued.some((k) => k.includes('quote:AAPL')));
|
||||
assert.ok(queued.some((k) => k.includes('expiry_dates:AAPL')));
|
||||
});
|
||||
|
||||
test('scheduleDealerMapInputs caps chain jobs to maxExpiries', async () => {
|
||||
const db = memDb();
|
||||
db.prepare(
|
||||
'INSERT INTO kv_cache (key,value,observed_at) VALUES (?,?,?)',
|
||||
).run(
|
||||
'options_expiry:QQQ',
|
||||
JSON.stringify(['2026-08-08', '2026-08-15', '2026-08-22', '2026-08-29', '2026-09-05', '2026-09-12', '2026-09-19', '2026-10-17']),
|
||||
'2026-08-06T14:00:00.000Z',
|
||||
);
|
||||
const queued: string[] = [];
|
||||
const queue = { queue: async (key: string) => { queued.push(key); } } as any;
|
||||
await scheduleDealerMapInputs({ db, queue, cache: { ensureInDemand: async () => {} } as any }, 'QQQ', 4);
|
||||
const chains = queued.filter((k) => k.startsWith('yfinance:chain:QQQ:'));
|
||||
assert.equal(chains.length, 4);
|
||||
});
|
||||
|
||||
test('getDealerMap returns stale cached map when chains missing', async () => {
|
||||
const db = memDb();
|
||||
const synthetic: NormalizedOptionSurface = {
|
||||
symbol: 'MSFT',
|
||||
spot: 400,
|
||||
asOf: '2026-08-01T15:00:00.000Z',
|
||||
providerId: 'synthetic',
|
||||
delaySeconds: 900,
|
||||
oiFreshness: 'prior_session',
|
||||
rows: [
|
||||
{
|
||||
strike: 400,
|
||||
expiry: '2026-08-15',
|
||||
right: 'call',
|
||||
openInterest: 1000,
|
||||
volume: 10,
|
||||
bid: 1,
|
||||
ask: 1.1,
|
||||
mid: 1.05,
|
||||
iv: 0.25,
|
||||
greeks: { gamma: 0.02, vanna: 0.05 },
|
||||
},
|
||||
],
|
||||
provenance: { fetchedAt: '2026-08-01T15:00:00.000Z', sourceKind: 'yfinance' },
|
||||
};
|
||||
const map = buildDealerMap(synthetic);
|
||||
writeDealerMapCache(db, map);
|
||||
|
||||
const queued: string[] = [];
|
||||
const result = await getDealerMap(
|
||||
{ db, queue: { queue: async (k: string) => { queued.push(k); } } as any, cache: { ensureInDemand: async () => {} } as any },
|
||||
'MSFT',
|
||||
);
|
||||
assert.ok(result.map);
|
||||
// Cached thin synthetic map is incomplete by integrity gates, but still served as last map.
|
||||
assert.ok(result.status === 'stale' || result.status === 'incomplete', result.status);
|
||||
assert.ok(queued.length > 0);
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
gradeStudySetup,
|
||||
proposeStudySetups,
|
||||
type CandleBar,
|
||||
} from '../dealerStudyEngine.ts';
|
||||
import type { DealerMap } from '../dealerExposureEngine.ts';
|
||||
|
||||
function baseMap(over: Partial<DealerMap> = {}): DealerMap {
|
||||
return {
|
||||
symbol: 'TEST',
|
||||
spot: 100,
|
||||
asOf: '2026-08-01T00:00:00.000Z',
|
||||
delayNote: 'delayed',
|
||||
delaySeconds: 900,
|
||||
oiFreshness: 'prior_session',
|
||||
providerId: 'yfinance',
|
||||
convention: 'classic_retail',
|
||||
unit: 'usd',
|
||||
computation: 'settled_oi',
|
||||
expiries: ['2026-08-15'],
|
||||
cells: [],
|
||||
byStrike: [],
|
||||
netVexByStrike: [],
|
||||
levels: {
|
||||
king: { strike: 105, expiry: '2026-08-15', gex: 1e6 },
|
||||
gatekeepers: [],
|
||||
flip: 98,
|
||||
callWall: 108,
|
||||
putWall: 95,
|
||||
airPockets: [],
|
||||
clusters: [],
|
||||
},
|
||||
regimeAtSpot: 'positive',
|
||||
scenario: 'gex_dominates',
|
||||
patterns: [],
|
||||
opexHints: { isNearOpex: false, note: '' },
|
||||
provenance: { source: 'test' } as DealerMap['provenance'],
|
||||
coveragePct: 90,
|
||||
bsmFillPct: 80,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe('proposeStudySetups', () => {
|
||||
it('proposes cushion pin when positive regime and king away from spot', () => {
|
||||
const props = proposeStudySetups(baseMap());
|
||||
assert.ok(props.some((p) => p.hypothesisId === 'cushion_pin_toward_king'));
|
||||
assert.ok(props.every((p) => p.disclaimer === 'Educational observation only.'));
|
||||
});
|
||||
|
||||
it('proposes spring setup when negative regime', () => {
|
||||
const props = proposeStudySetups(baseMap({ regimeAtSpot: 'negative' }));
|
||||
assert.ok(props.some((p) => p.hypothesisId === 'spring_accelerate_from_spot'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('gradeStudySetup', () => {
|
||||
const loggedAt = '2026-08-01T00:00:00.000Z';
|
||||
|
||||
it('grades long win when target before invalidation', () => {
|
||||
const bars: CandleBar[] = [
|
||||
{ ts: '2026-08-02', o: 100, h: 101, l: 99, c: 100.5 },
|
||||
{ ts: '2026-08-03', o: 100.5, h: 106, l: 100, c: 105 },
|
||||
];
|
||||
const r = gradeStudySetup(
|
||||
{
|
||||
bias: 'educational_long',
|
||||
entry: 100,
|
||||
invalidation: 95,
|
||||
target: 105,
|
||||
horizonDays: 5,
|
||||
loggedAt,
|
||||
},
|
||||
bars,
|
||||
new Date('2026-08-10'),
|
||||
);
|
||||
assert.equal(r.grade, 'win');
|
||||
assert.ok(r.score >= 55);
|
||||
});
|
||||
|
||||
it('grades long loss when invalidation first', () => {
|
||||
const bars: CandleBar[] = [
|
||||
{ ts: '2026-08-02', o: 100, h: 100.5, l: 94, c: 95 },
|
||||
{ ts: '2026-08-03', o: 95, h: 110, l: 94, c: 108 },
|
||||
];
|
||||
const r = gradeStudySetup(
|
||||
{
|
||||
bias: 'educational_long',
|
||||
entry: 100,
|
||||
invalidation: 95,
|
||||
target: 105,
|
||||
horizonDays: 5,
|
||||
loggedAt,
|
||||
},
|
||||
bars,
|
||||
new Date('2026-08-10'),
|
||||
);
|
||||
assert.equal(r.grade, 'loss');
|
||||
});
|
||||
|
||||
it('grades range win if held inside edges', () => {
|
||||
const bars: CandleBar[] = [
|
||||
{ ts: '2026-08-02', o: 100, h: 102, l: 98, c: 100 },
|
||||
{ ts: '2026-08-03', o: 100, h: 103, l: 97, c: 101 },
|
||||
{ ts: '2026-08-04', o: 101, h: 102, l: 99, c: 100 },
|
||||
{ ts: '2026-08-05', o: 100, h: 101, l: 99, c: 100 },
|
||||
{ ts: '2026-08-06', o: 100, h: 102, l: 98, c: 100 },
|
||||
];
|
||||
const r = gradeStudySetup(
|
||||
{
|
||||
bias: 'educational_range',
|
||||
entry: 100,
|
||||
invalidation: 95,
|
||||
target: 105,
|
||||
horizonDays: 5,
|
||||
loggedAt,
|
||||
},
|
||||
bars,
|
||||
new Date('2026-08-10'),
|
||||
);
|
||||
assert.equal(r.grade, 'win');
|
||||
});
|
||||
|
||||
it('returns incomplete when no bars', () => {
|
||||
const r = gradeStudySetup(
|
||||
{
|
||||
bias: 'educational_long',
|
||||
entry: 100,
|
||||
invalidation: 95,
|
||||
target: 105,
|
||||
horizonDays: 5,
|
||||
loggedAt,
|
||||
},
|
||||
[],
|
||||
new Date('2026-08-10'),
|
||||
);
|
||||
assert.equal(r.grade, 'incomplete');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { extractHanStyleLevels } from '../hanStyleLevels.ts';
|
||||
import {
|
||||
buildDealerMap,
|
||||
withExposureConvention,
|
||||
type DealerMap,
|
||||
} from '../dealerExposureEngine.ts';
|
||||
import type { NormalizedOptionSurface, NormalizedOptionRow } from '../../options/types.ts';
|
||||
|
||||
function row(
|
||||
partial: Partial<NormalizedOptionRow> & Pick<NormalizedOptionRow, 'strike' | 'expiry' | 'right'>,
|
||||
): NormalizedOptionRow {
|
||||
return {
|
||||
openInterest: 1000,
|
||||
volume: 100,
|
||||
bid: 1,
|
||||
ask: 1.1,
|
||||
mid: 1.05,
|
||||
iv: 0.25,
|
||||
greeks: { gamma: 0.02, vanna: 0.1, delta: 0.5 },
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
function surface(rows: NormalizedOptionRow[], spot = 40): NormalizedOptionSurface {
|
||||
return {
|
||||
symbol: 'IREN',
|
||||
spot,
|
||||
asOf: '2026-08-07T15:00:00.000Z',
|
||||
providerId: 'synthetic',
|
||||
delaySeconds: 900,
|
||||
oiFreshness: 'prior_session',
|
||||
rows,
|
||||
provenance: {
|
||||
fetchedAt: '2026-08-07T15:00:00.000Z',
|
||||
sourceKind: 'yfinance',
|
||||
rawSourceId: 'synthetic:IREN',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Han-like stack: long support below, short king lower, resist above. */
|
||||
function irenLikeMap(spot = 41.2): DealerMap {
|
||||
const expNear = '2026-08-15';
|
||||
const expMid = '2026-08-22';
|
||||
const expFar = '2026-09-19';
|
||||
return buildDealerMap(
|
||||
surface(
|
||||
[
|
||||
// Strong +GEX at 38 (long support below spot)
|
||||
row({
|
||||
strike: 38,
|
||||
expiry: expNear,
|
||||
right: 'call',
|
||||
openInterest: 12_000,
|
||||
greeks: { gamma: 0.05, vanna: 0.1, delta: 0.4 },
|
||||
}),
|
||||
row({
|
||||
strike: 38,
|
||||
expiry: expNear,
|
||||
right: 'put',
|
||||
openInterest: 2_000,
|
||||
greeks: { gamma: 0.02, vanna: 0.05, delta: -0.3 },
|
||||
}),
|
||||
// Short / put heavy at 34
|
||||
row({
|
||||
strike: 34,
|
||||
expiry: expNear,
|
||||
right: 'put',
|
||||
openInterest: 15_000,
|
||||
greeks: { gamma: 0.04, vanna: 0.08, delta: -0.55 },
|
||||
}),
|
||||
row({
|
||||
strike: 34,
|
||||
expiry: expNear,
|
||||
right: 'call',
|
||||
openInterest: 1_000,
|
||||
greeks: { gamma: 0.01, vanna: 0.05, delta: 0.2 },
|
||||
}),
|
||||
// Resistance pile at 45 (heavy calls above)
|
||||
row({
|
||||
strike: 45,
|
||||
expiry: expNear,
|
||||
right: 'call',
|
||||
openInterest: 10_000,
|
||||
greeks: { gamma: 0.04, vanna: 0.1, delta: 0.35 },
|
||||
}),
|
||||
row({
|
||||
strike: 45,
|
||||
expiry: expNear,
|
||||
right: 'put',
|
||||
openInterest: 1_500,
|
||||
greeks: { gamma: 0.015, vanna: 0.05, delta: -0.25 },
|
||||
}),
|
||||
// ATM mixed
|
||||
row({
|
||||
strike: 41,
|
||||
expiry: expNear,
|
||||
right: 'call',
|
||||
openInterest: 4_000,
|
||||
greeks: { gamma: 0.03, vanna: 0.1, delta: 0.5 },
|
||||
}),
|
||||
row({
|
||||
strike: 41,
|
||||
expiry: expNear,
|
||||
right: 'put',
|
||||
openInterest: 3_500,
|
||||
greeks: { gamma: 0.03, vanna: 0.08, delta: -0.5 },
|
||||
}),
|
||||
// Far OPEX structure lower (35 / 30)
|
||||
row({
|
||||
strike: 35,
|
||||
expiry: expFar,
|
||||
right: 'put',
|
||||
openInterest: 8_000,
|
||||
greeks: { gamma: 0.025, vanna: 0.06, delta: -0.5 },
|
||||
}),
|
||||
row({
|
||||
strike: 30,
|
||||
expiry: expFar,
|
||||
right: 'put',
|
||||
openInterest: 7_000,
|
||||
greeks: { gamma: 0.02, vanna: 0.05, delta: -0.6 },
|
||||
}),
|
||||
row({
|
||||
strike: 38,
|
||||
expiry: expMid,
|
||||
right: 'call',
|
||||
openInterest: 5_000,
|
||||
greeks: { gamma: 0.03, vanna: 0.08, delta: 0.4 },
|
||||
}),
|
||||
],
|
||||
spot,
|
||||
),
|
||||
{ maxExpiries: 6 },
|
||||
);
|
||||
}
|
||||
|
||||
test('extractHanStyleLevels: long support / resist / short vocabulary', () => {
|
||||
const map = irenLikeMap(41.2);
|
||||
const han = extractHanStyleLevels(map);
|
||||
|
||||
assert.equal(han.symbol, 'IREN');
|
||||
assert.equal(han.convention, 'classic_call_pos_put_neg');
|
||||
assert.ok(han.longSupport != null, 'expects long support below spot');
|
||||
assert.ok(han.longSupport! <= map.spot + 1e-6);
|
||||
assert.ok(han.resistance != null, 'expects resistance above or at tough level');
|
||||
assert.ok(han.shortKing != null, 'expects a short king somewhere');
|
||||
|
||||
const blob = `${han.script}\n${han.chips.join('\n')}`.toLowerCase();
|
||||
assert.match(blob, /long/);
|
||||
assert.ok(
|
||||
/support|resist|short|opex/.test(blob),
|
||||
`expected Han vocabulary in chips/script, got: ${blob}`,
|
||||
);
|
||||
assert.match(han.script, /educational observation only/i);
|
||||
assert.ok(han.chips.length >= 1);
|
||||
});
|
||||
|
||||
test('extractHanStyleLevels: dealer convention flips signs and updates script', () => {
|
||||
const classic = irenLikeMap(41.2);
|
||||
const dealer = withExposureConvention(classic, 'dealer_inventory');
|
||||
const hClassic = extractHanStyleLevels(classic);
|
||||
const hDealer = extractHanStyleLevels(dealer);
|
||||
|
||||
assert.equal(hDealer.convention, 'dealer_inventory');
|
||||
assert.match(hDealer.script, /dealer\/hs/i);
|
||||
assert.match(hClassic.script, /classic/i);
|
||||
|
||||
// Full sign flip → long/short labels at a strike can swap
|
||||
if (hClassic.longSupport != null && hDealer.shortKing != null) {
|
||||
// Not always equal strikes, but regime or king sign must differ somewhere
|
||||
const sameSupport =
|
||||
hClassic.longSupport === hDealer.longSupport &&
|
||||
hClassic.shortKing === hDealer.shortKing &&
|
||||
hClassic.regimeLabel === hDealer.regimeLabel;
|
||||
assert.equal(sameSupport, false, 'dealer flip should change Han levels or regime');
|
||||
}
|
||||
});
|
||||
|
||||
test('extractHanStyleLevels: empty-ish map still returns safe script', () => {
|
||||
const thin = buildDealerMap(
|
||||
surface([
|
||||
row({
|
||||
strike: 40,
|
||||
expiry: '2026-08-15',
|
||||
right: 'call',
|
||||
openInterest: 50,
|
||||
greeks: { gamma: 0.01, vanna: 0.02, delta: 0.5 },
|
||||
}),
|
||||
], 40),
|
||||
);
|
||||
const han = extractHanStyleLevels(thin);
|
||||
assert.equal(han.symbol, 'IREN');
|
||||
assert.ok(han.script.length > 20);
|
||||
assert.match(han.script, /educational observation only/i);
|
||||
assert.ok(['long_gamma', 'short_gamma', 'mixed'].includes(han.regimeLabel));
|
||||
});
|
||||
|
||||
test('extractHanStyleLevels: no buy/sell imperatives', () => {
|
||||
const han = extractHanStyleLevels(irenLikeMap(41.2));
|
||||
const bad = /\b(buy|sell|short it|long it|enter|exit|target)\b/i;
|
||||
assert.ok(!bad.test(han.script), `imperative found: ${han.script}`);
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import {
|
||||
totalReturnPct,
|
||||
dayChangePct,
|
||||
returnsForHorizons,
|
||||
relativeStrength,
|
||||
buildSectorRsMap,
|
||||
summarizeRotation,
|
||||
@@ -25,6 +27,27 @@ test('totalReturnPct: computes window return', () => {
|
||||
assert.ok(Math.abs((r as number) - 10) < 1.5);
|
||||
});
|
||||
|
||||
test('dayChangePct: last close vs previous close', () => {
|
||||
assert.equal(dayChangePct(candles([100, 110])), 10);
|
||||
assert.equal(dayChangePct(candles([100, 90])), -10);
|
||||
assert.equal(dayChangePct(candles([100])), null);
|
||||
});
|
||||
|
||||
test('returnsForHorizons oneDay: not zero when 24h-ago target falls on a gap (13:30Z bars, Monday)', () => {
|
||||
// yfinance daily bars carry 13:30Z timestamps. Latest = Monday 08-03, prev = Friday 07-31.
|
||||
// A window-based oneDay (latestTs − 24h) lands on Sunday — closer to the latest candle
|
||||
// than to Friday — which used to produce exactly 0.
|
||||
const ts = (d: string) => new Date(`${d}T13:30:00.000Z`).toISOString();
|
||||
const bars: CandlePoint[] = [
|
||||
{ ts: ts('2026-07-30'), c: 100, v: 100 },
|
||||
{ ts: ts('2026-07-31'), c: 102, v: 100 },
|
||||
{ ts: ts('2026-08-03'), c: 104, v: 100 }, // Monday
|
||||
];
|
||||
const r = returnsForHorizons(bars);
|
||||
assert.notEqual(r.oneDay, 0);
|
||||
assert.ok(Math.abs((r.oneDay as number) - (104 / 102 - 1) * 100) < 1e-9);
|
||||
});
|
||||
|
||||
test('relativeStrength: sector minus bench', () => {
|
||||
assert.equal(relativeStrength(12, 5), 7);
|
||||
assert.equal(relativeStrength(null, 5), null);
|
||||
@@ -72,5 +95,5 @@ test('summarizeRotation: produces educational summary without trade verbs', () =
|
||||
const s = summarizeRotation(rows);
|
||||
assert.ok(s.leadershipSpread > 0 || s.strength === 'none' || s.leadingCount >= 0);
|
||||
assert.ok(!/you should|buy |sell /i.test(s.summary));
|
||||
assert.ok(/outperform|underperform|leadership|educational|broad market/i.test(s.summary));
|
||||
assert.ok(/outperform|underperform|leadership|relative|broad market/i.test(s.summary));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user