Emit structure flow packets with full evidence

This commit is contained in:
dirtydishes 2026-01-28 20:36:43 -05:00
parent f08abec68a
commit fe6aef5fbc
6 changed files with 538 additions and 54 deletions

View file

@ -137,6 +137,7 @@ describe("classifier structure and positioning signals", () => {
test("straddle classifier triggers on structure tag", () => {
const packet = buildPacket({
packet_kind: "structure",
structure_type: "straddle",
structure_legs: 2,
structure_strikes: 1,
@ -147,8 +148,21 @@ describe("classifier structure and positioning signals", () => {
expect(hits.some((hit) => hit.classifier_id === "straddle")).toBe(true);
});
test("structure classifiers are suppressed on per-contract packets", () => {
const packet = buildPacket({
structure_type: "straddle",
structure_legs: 2,
structure_strikes: 1,
structure_rights: "C/P",
structure_strike_span: 0
});
const hits = evaluateClassifiers(packet, baseConfig);
expect(hits.some((hit) => hit.classifier_id === "straddle")).toBe(false);
});
test("vertical spread infers direction from aggressor skew", () => {
const packet = buildPacket({
packet_kind: "structure",
structure_type: "vertical",
structure_legs: 2,
structure_strikes: 2,
@ -167,6 +181,7 @@ describe("classifier structure and positioning signals", () => {
test("ladder accumulation triggers on multi-strike structures", () => {
const packet = buildPacket({
packet_kind: "structure",
structure_type: "ladder",
structure_legs: 3,
structure_strikes: 3,

View file

@ -0,0 +1,137 @@
import { describe, expect, test } from "bun:test";
import { summarizeStructure } from "../src/structures";
import {
buildStructureFlowPacket,
planStructurePacket,
shouldEmitStructurePacket,
type LegEvidence
} from "../src/structure-packets";
const placements = (overrides?: Partial<LegEvidence["placements"]>): LegEvidence["placements"] => ({
aa: 0,
a: 0,
b: 0,
bb: 0,
mid: 0,
missing: 0,
stale: 0,
...overrides
});
const leg = (input: Partial<LegEvidence> & Pick<LegEvidence, "contractId" | "right" | "strike">): LegEvidence => {
return {
contractId: input.contractId,
root: "SPY",
expiry: "2025-01-17",
right: input.right,
strike: input.strike,
startTs: input.startTs ?? 1000,
endTs: input.endTs ?? 1100,
members: input.members ?? [input.contractId],
totalSize: input.totalSize ?? 100,
totalPremium: input.totalPremium ?? 1000,
placements: input.placements ?? placements(),
source_ts: input.source_ts ?? 1000,
ingest_ts: input.ingest_ts ?? 1200,
seq: input.seq ?? 1
};
};
describe("structure packet planning", () => {
test("emits only on latest leg endTs", () => {
const call = leg({
contractId: "SPY-2025-01-17-450-C",
right: "C",
strike: 450,
endTs: 1100
});
const put = leg({
contractId: "SPY-2025-01-17-450-P",
right: "P",
strike: 450,
endTs: 1125
});
const legs: LegEvidence[] = [call, put];
expect(shouldEmitStructurePacket(legs, call.contractId)).toBe(false);
expect(shouldEmitStructurePacket(legs, put.contractId)).toBe(true);
});
test("plans deterministic id + members across legs", () => {
const call = leg({
contractId: "SPY-2025-01-17-450-C",
right: "C",
strike: 450,
members: ["p2", "p1"],
totalSize: 20,
totalPremium: 4000,
placements: placements({ aa: 1, mid: 1 })
});
const put = leg({
contractId: "SPY-2025-01-17-450-P",
right: "P",
strike: 450,
startTs: 1005,
endTs: 1120,
members: ["p3"],
totalSize: 10,
totalPremium: 1500,
placements: placements({ bb: 1 })
});
const legs = [call, put];
const summary = summarizeStructure(legs);
expect(summary?.type).toBe("straddle");
const plan = planStructurePacket(legs, summary!, 500);
expect(plan).not.toBeNull();
expect(plan!.pseudoContractId).toBe("SPY-2025-01-17-STRUCT-straddle");
expect(plan!.id.startsWith("flowpacket:SPY-2025-01-17-STRUCT-straddle:")).toBe(true);
expect(plan!.members).toEqual(["p1", "p2", "p3"]);
expect(plan!.totalSize).toBe(30);
expect(plan!.totalPremium).toBe(5500);
expect(plan!.count).toBe(3);
const swappedPlan = planStructurePacket([put, call], summary!, 500);
expect(swappedPlan).not.toBeNull();
expect(swappedPlan!.id).toBe(plan!.id);
expect(swappedPlan!.members).toEqual(plan!.members);
});
test("builds structure FlowPacket with aggregate aggressor ratios", () => {
const call = leg({
contractId: "SPY-2025-01-17-450-C",
right: "C",
strike: 450,
members: ["p1", "p2"],
totalSize: 20,
totalPremium: 4000,
placements: placements({ aa: 1, mid: 1 })
});
const put = leg({
contractId: "SPY-2025-01-17-450-P",
right: "P",
strike: 450,
members: ["p3"],
totalSize: 10,
totalPremium: 1500,
placements: placements({ bb: 1 })
});
const legs = [call, put];
const summary = summarizeStructure(legs);
const plan = planStructurePacket(legs, summary!, 500);
const packet = buildStructureFlowPacket(plan!, summary!);
expect(packet.features.packet_kind).toBe("structure");
expect(packet.features.underlying_id).toBe("SPY");
expect(packet.features.nbbo_aa_count).toBe(1);
expect(packet.features.nbbo_bb_count).toBe(1);
expect(packet.features.nbbo_mid_count).toBe(1);
expect(packet.features.nbbo_coverage_ratio).toBeCloseTo(1, 6);
// 2 aggressive (AA + BB) out of 3 classified (AA + BB + MID)
expect(packet.features.nbbo_aggressive_ratio).toBeCloseTo(2 / 3, 4);
});
});