From 9076d3b3953c80d59fefe87d6fac51a1f6265ee5 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Wed, 13 May 2026 22:36:13 -0400 Subject: [PATCH] Add synthetic print and structure features - Export synthetic market types - Track special print conditions and derived cluster features - Add same-size leg symmetry to structure packets --- packages/types/src/index.ts | 1 + services/compute/src/index.ts | 85 +++++++++++++++++++ services/compute/src/structure-packets.ts | 16 ++++ .../compute/tests/structure-packets.test.ts | 1 + 4 files changed, 103 insertions(+) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ce55e57..af22365 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,3 +2,4 @@ export * from "./events"; export * from "./live"; export * from "./options-flow"; export * from "./sp500"; +export * from "./synthetic-market"; diff --git a/services/compute/src/index.ts b/services/compute/src/index.ts index d2e58b0..8f01c7a 100644 --- a/services/compute/src/index.ts +++ b/services/compute/src/index.ts @@ -271,6 +271,14 @@ type ClusterState = { totalPremium: number; firstPrice: number; lastPrice: number; + conditions: Set; + specialPrintCount: number; + firstExecutionIv: number | null; + lastExecutionIv: number | null; + minExecutionIv: number | null; + maxExecutionIv: number | null; + firstUnderlyingMid: number | null; + lastUnderlyingMid: number | null; placements: NbboPlacementCounts; flushed: boolean; }; @@ -329,6 +337,29 @@ const createPlacementCounts = (): NbboPlacementCounts => ({ stale: 0 }); +const SPECIAL_PRINT_CONDITIONS = new Set(["AUCTION", "CROSS", "OPENING", "CLOSING", "COMPLEX", "SPREAD"]); +const SYNTHETIC_EVENT_CONDITION_RE = /^EVENT_(\d+)D$/i; + +const normalizeConditions = (conditions: readonly string[] | undefined): string[] => + (conditions ?? []).map((condition) => condition.trim().toUpperCase()).filter(Boolean); + +const hasSpecialCondition = (conditions: readonly string[] | undefined): boolean => + normalizeConditions(conditions).some((condition) => SPECIAL_PRINT_CONDITIONS.has(condition)); + +const parseSyntheticEventOffsetDays = (conditions: Iterable): number | null => { + for (const condition of conditions) { + const match = SYNTHETIC_EVENT_CONDITION_RE.exec(condition); + if (!match) { + continue; + } + const days = Number(match[1]); + if (Number.isFinite(days) && days > 0) { + return days; + } + } + return null; +}; + const recordPlacement = (counts: NbboPlacementCounts, placement: NbboPlacement): void => { switch (placement) { case "AA": @@ -569,6 +600,12 @@ const applyDeliverPolicy = ( const buildCluster = (print: OptionPrint): ClusterState => { const placements = createPlacementCounts(); + const normalizedConditions = normalizeConditions(print.conditions); + const executionIv = typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv) ? print.execution_iv : null; + const executionUnderlyingMid = + typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid) + ? print.execution_underlying_mid + : null; recordPlacement(placements, classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts))); return { contractId: print.option_contract_id, @@ -585,6 +622,14 @@ const buildCluster = (print: OptionPrint): ClusterState => { totalPremium: print.price * print.size, firstPrice: print.price, lastPrice: print.price, + conditions: new Set(normalizedConditions), + specialPrintCount: hasSpecialCondition(print.conditions) ? 1 : 0, + firstExecutionIv: executionIv, + lastExecutionIv: executionIv, + minExecutionIv: executionIv, + maxExecutionIv: executionIv, + firstUnderlyingMid: executionUnderlyingMid, + lastUnderlyingMid: executionUnderlyingMid, placements, flushed: false }; @@ -607,6 +652,25 @@ const updateCluster = (cluster: ClusterState, print: OptionPrint): ClusterState cluster.totalSize += print.size; cluster.totalPremium += print.price * print.size; cluster.lastPrice = print.price; + for (const condition of normalizeConditions(print.conditions)) { + cluster.conditions.add(condition); + } + if (hasSpecialCondition(print.conditions)) { + cluster.specialPrintCount += 1; + } + if (typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv)) { + cluster.lastExecutionIv = print.execution_iv; + cluster.minExecutionIv = + cluster.minExecutionIv === null ? print.execution_iv : Math.min(cluster.minExecutionIv, print.execution_iv); + cluster.maxExecutionIv = + cluster.maxExecutionIv === null ? print.execution_iv : Math.max(cluster.maxExecutionIv, print.execution_iv); + } + if (typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)) { + if (cluster.firstUnderlyingMid === null) { + cluster.firstUnderlyingMid = print.execution_underlying_mid; + } + cluster.lastUnderlyingMid = print.execution_underlying_mid; + } recordPlacement( cluster.placements, classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts)) @@ -836,6 +900,27 @@ const flushCluster = async ( if (cluster.isEtf !== null) { features.is_etf = cluster.isEtf; } + if (cluster.conditions.size > 0) { + features.conditions = Array.from(cluster.conditions).sort().join(","); + } + if (cluster.specialPrintCount > 0) { + features.special_print_count = cluster.specialPrintCount; + } + if (cluster.minExecutionIv !== null && cluster.maxExecutionIv !== null) { + features.execution_iv_shock = roundTo(Math.max(0, cluster.maxExecutionIv - cluster.minExecutionIv)); + } + if ( + cluster.firstUnderlyingMid !== null && + cluster.lastUnderlyingMid !== null && + cluster.firstUnderlyingMid > 0 + ) { + const moveBps = ((cluster.lastUnderlyingMid - cluster.firstUnderlyingMid) / cluster.firstUnderlyingMid) * 10_000; + features.underlying_move_bps = roundTo(moveBps); + } + const syntheticEventOffsetDays = parseSyntheticEventOffsetDays(cluster.conditions); + if (syntheticEventOffsetDays !== null) { + features.corporate_event_ts = cluster.endTs + syntheticEventOffsetDays * 86_400_000; + } const placementTotal = cluster.placements.aa + diff --git a/services/compute/src/structure-packets.ts b/services/compute/src/structure-packets.ts index a168880..82876f7 100644 --- a/services/compute/src/structure-packets.ts +++ b/services/compute/src/structure-packets.ts @@ -46,6 +46,7 @@ export type StructurePacketPlan = { nbboAggressiveBuyRatio: number; nbboAggressiveSellRatio: number; nbboAggressiveRatio: number; + sameSizeLegSymmetry: number; source_ts: number; ingest_ts: number; seq: number; @@ -132,6 +133,19 @@ const dayDiff = (from: string | null, to: string | null): number | null => { return Math.round(diffMs / 86_400_000); }; +const sameSizeLegSymmetry = (legs: LegEvidence[]): number => { + const sizes = legs.map((leg) => leg.totalSize).filter((value) => Number.isFinite(value) && value > 0); + if (sizes.length < 2) { + return 0; + } + const min = Math.min(...sizes); + const max = Math.max(...sizes); + if (!Number.isFinite(min) || !Number.isFinite(max) || max <= 0) { + return 0; + } + return min / max; +}; + export const shouldEmitStructurePacket = (legs: LegEvidence[], currentLegContractId: string): boolean => { if (legs.length < 2) { return false; @@ -250,6 +264,7 @@ export const planStructurePacket = ( nbboAggressiveBuyRatio, nbboAggressiveSellRatio, nbboAggressiveRatio, + sameSizeLegSymmetry: roundTo(sameSizeLegSymmetry(legs)), source_ts: Number.isFinite(source_ts) ? source_ts : 0, ingest_ts, seq @@ -320,6 +335,7 @@ export const buildStructureFlowPacket = ( features.nbbo_aggressive_buy_ratio = roundTo(plan.nbboAggressiveBuyRatio); features.nbbo_aggressive_sell_ratio = roundTo(plan.nbboAggressiveSellRatio); features.nbbo_aggressive_ratio = roundTo(plan.nbboAggressiveRatio); + features.same_size_leg_symmetry = roundTo(plan.sameSizeLegSymmetry); const join_quality: Record = { nbbo_coverage_ratio: roundTo(plan.nbboCoverageRatio) diff --git a/services/compute/tests/structure-packets.test.ts b/services/compute/tests/structure-packets.test.ts index 0ee20a8..80dfa81 100644 --- a/services/compute/tests/structure-packets.test.ts +++ b/services/compute/tests/structure-packets.test.ts @@ -130,6 +130,7 @@ describe("structure packet planning", () => { 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); + expect(packet.features.same_size_leg_symmetry).toBeCloseTo(0.5, 4); // 2 aggressive (AA + BB) out of 3 classified (AA + BB + MID) expect(packet.features.nbbo_aggressive_ratio).toBeCloseTo(2 / 3, 4);