142 lines
3.7 KiB
TypeScript
142 lines
3.7 KiB
TypeScript
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');
|
||
|
|
});
|
||
|
|
});
|