feat: dealer flow, mirror portfolio (M21), options convexity, FINRA short interest, alert producers, vendor gate
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:
Investor Flow Build
2026-08-10 13:36:26 -04:00
parent 04fc11b2fd
commit ac94acf9e3
229 changed files with 32617 additions and 3934 deletions
@@ -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));
});