Files
investor-flow/app/server/src/analysis/__tests__/dealerExposureEngine.test.ts
T

429 lines
15 KiB
TypeScript
Raw Normal View History

import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import {
buildDealerMap,
contractGex,
contractVex,
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('contractVex uses the same S² notional as GEX', () => {
const gex = contractGex(0.02, 1000, 44, 'call');
const vex = contractVex(0.02, 1000, 44, 'call');
assert.equal(vex, gex);
assert.equal(contractVex(0.02, 1000, 44, 'put'), -vex);
});
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);
});