Rebuild synthetic smart-money scenarios
This commit is contained in:
parent
6b794ec7ac
commit
86661df7ae
4 changed files with 468 additions and 7 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import {
|
||||
SP500_SYMBOLS,
|
||||
type FlowPacket,
|
||||
type OptionNBBO,
|
||||
type OptionPrint,
|
||||
type SmartMoneyProfileId,
|
||||
type SyntheticMarketMode
|
||||
} from "@islandflow/types";
|
||||
import type { OptionIngestAdapter, OptionIngestHandlers } from "./types";
|
||||
|
|
@ -23,7 +25,9 @@ type Burst = {
|
|||
printCount: number;
|
||||
priceStep: number;
|
||||
scenarioId: string;
|
||||
label: SyntheticScenarioLabel;
|
||||
seed: number;
|
||||
flowFeatures: FlowPacket["features"];
|
||||
};
|
||||
|
||||
export type SyntheticContractIvState = {
|
||||
|
|
@ -58,73 +62,157 @@ type WeightedValue<T> = {
|
|||
type Scenario = {
|
||||
id: string;
|
||||
weight: number;
|
||||
label: SyntheticScenarioLabel;
|
||||
right: "C" | "P" | "either";
|
||||
countRange: [number, number];
|
||||
sizeRange: [number, number];
|
||||
targetNotionalRange: [number, number];
|
||||
priceTrend: "up" | "down" | "flat";
|
||||
expiryOffsets?: number[];
|
||||
underlying?: number;
|
||||
strikeMoneyness?: number;
|
||||
flowFeatures: FlowPacket["features"];
|
||||
conditions?: string[];
|
||||
};
|
||||
|
||||
export type SyntheticScenarioLabel = SmartMoneyProfileId | "neutral_noise";
|
||||
|
||||
export type SyntheticSmartMoneyScenario = {
|
||||
id: string;
|
||||
label: SyntheticScenarioLabel;
|
||||
hiddenLabel: SyntheticScenarioLabel;
|
||||
};
|
||||
|
||||
const SMART_MONEY_SCENARIO_IDS = [
|
||||
"institutional_directional",
|
||||
"retail_whale",
|
||||
"event_driven",
|
||||
"vol_seller",
|
||||
"arbitrage",
|
||||
"hedge_reactive",
|
||||
"neutral_noise"
|
||||
] as const;
|
||||
|
||||
const REALISTIC_SCENARIOS: Scenario[] = [
|
||||
{
|
||||
id: "ask_lift",
|
||||
weight: 18,
|
||||
label: "institutional_directional",
|
||||
right: "either",
|
||||
countRange: [1, 2],
|
||||
sizeRange: [30, 180],
|
||||
targetNotionalRange: [9_000, 35_000],
|
||||
priceTrend: "flat",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.88,
|
||||
nbbo_aggressive_ratio: 0.7,
|
||||
nbbo_aggressive_buy_ratio: 0.66,
|
||||
nbbo_aggressive_sell_ratio: 0.08,
|
||||
nbbo_inside_ratio: 0.12,
|
||||
venue_count: 2
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
},
|
||||
{
|
||||
id: "mid_block",
|
||||
weight: 14,
|
||||
label: "arbitrage",
|
||||
right: "either",
|
||||
countRange: [1, 2],
|
||||
sizeRange: [120, 480],
|
||||
targetNotionalRange: [12_000, 45_000],
|
||||
priceTrend: "flat",
|
||||
flowFeatures: {
|
||||
structure_type: "vertical",
|
||||
structure_legs: 2,
|
||||
structure_strikes: 2,
|
||||
same_size_leg_symmetry: 0.74,
|
||||
nbbo_coverage_ratio: 0.82,
|
||||
nbbo_aggressive_ratio: 0.26,
|
||||
nbbo_aggressive_buy_ratio: 0.3,
|
||||
nbbo_aggressive_sell_ratio: 0.24,
|
||||
nbbo_inside_ratio: 0.42,
|
||||
venue_count: 2
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
},
|
||||
{
|
||||
id: "bullish_sweep",
|
||||
weight: 8,
|
||||
label: "institutional_directional",
|
||||
right: "C",
|
||||
countRange: [2, 3],
|
||||
sizeRange: [180, 520],
|
||||
targetNotionalRange: [25_000, 90_000],
|
||||
priceTrend: "up",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.9,
|
||||
nbbo_aggressive_ratio: 0.82,
|
||||
nbbo_aggressive_buy_ratio: 0.78,
|
||||
nbbo_aggressive_sell_ratio: 0.04,
|
||||
nbbo_inside_ratio: 0.08,
|
||||
venue_count: 4
|
||||
},
|
||||
conditions: ["SWEEP"]
|
||||
},
|
||||
{
|
||||
id: "bearish_sweep",
|
||||
weight: 8,
|
||||
label: "institutional_directional",
|
||||
right: "P",
|
||||
countRange: [2, 3],
|
||||
sizeRange: [180, 520],
|
||||
targetNotionalRange: [25_000, 90_000],
|
||||
priceTrend: "up",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.9,
|
||||
nbbo_aggressive_ratio: 0.82,
|
||||
nbbo_aggressive_buy_ratio: 0.78,
|
||||
nbbo_aggressive_sell_ratio: 0.04,
|
||||
nbbo_inside_ratio: 0.08,
|
||||
venue_count: 4
|
||||
},
|
||||
conditions: ["SWEEP"]
|
||||
},
|
||||
{
|
||||
id: "contract_spike",
|
||||
weight: 6,
|
||||
label: "retail_whale",
|
||||
right: "either",
|
||||
countRange: [2, 3],
|
||||
sizeRange: [500, 900],
|
||||
targetNotionalRange: [18_000, 70_000],
|
||||
priceTrend: "flat",
|
||||
expiryOffsets: [0, 1, 7],
|
||||
strikeMoneyness: 1.08,
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.76,
|
||||
nbbo_aggressive_ratio: 0.68,
|
||||
nbbo_aggressive_buy_ratio: 0.62,
|
||||
nbbo_aggressive_sell_ratio: 0.08,
|
||||
nbbo_inside_ratio: 0.12,
|
||||
execution_iv_shock: 0.16,
|
||||
venue_count: 3
|
||||
},
|
||||
conditions: ["ISO"]
|
||||
},
|
||||
{
|
||||
id: "noise",
|
||||
weight: 46,
|
||||
label: "neutral_noise",
|
||||
right: "either",
|
||||
countRange: [1, 2],
|
||||
sizeRange: [5, 60],
|
||||
targetNotionalRange: [500, 6_000],
|
||||
priceTrend: "flat",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.76,
|
||||
nbbo_aggressive_ratio: 0.24,
|
||||
nbbo_aggressive_buy_ratio: 0.24,
|
||||
nbbo_aggressive_sell_ratio: 0.18,
|
||||
nbbo_inside_ratio: 0.52,
|
||||
venue_count: 1
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
}
|
||||
];
|
||||
|
|
@ -133,41 +221,246 @@ const ACTIVE_SCENARIOS: Scenario[] = [
|
|||
{
|
||||
id: "bullish_sweep",
|
||||
weight: 35,
|
||||
label: "institutional_directional",
|
||||
right: "C",
|
||||
countRange: [7, 10],
|
||||
sizeRange: [600, 1800],
|
||||
targetNotionalRange: [120_000, 240_000],
|
||||
priceTrend: "up",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.94,
|
||||
nbbo_aggressive_ratio: 0.86,
|
||||
nbbo_aggressive_buy_ratio: 0.82,
|
||||
nbbo_aggressive_sell_ratio: 0.03,
|
||||
nbbo_inside_ratio: 0.06,
|
||||
venue_count: 5
|
||||
},
|
||||
conditions: ["SWEEP"]
|
||||
},
|
||||
{
|
||||
id: "bearish_sweep",
|
||||
weight: 35,
|
||||
label: "institutional_directional",
|
||||
right: "P",
|
||||
countRange: [7, 10],
|
||||
sizeRange: [600, 1800],
|
||||
targetNotionalRange: [120_000, 240_000],
|
||||
priceTrend: "up",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.94,
|
||||
nbbo_aggressive_ratio: 0.86,
|
||||
nbbo_aggressive_buy_ratio: 0.82,
|
||||
nbbo_aggressive_sell_ratio: 0.03,
|
||||
nbbo_inside_ratio: 0.06,
|
||||
venue_count: 5
|
||||
},
|
||||
conditions: ["SWEEP"]
|
||||
},
|
||||
{
|
||||
id: "contract_spike",
|
||||
weight: 20,
|
||||
label: "retail_whale",
|
||||
right: "either",
|
||||
countRange: [5, 8],
|
||||
sizeRange: [1200, 3200],
|
||||
targetNotionalRange: [60_000, 140_000],
|
||||
priceTrend: "flat",
|
||||
expiryOffsets: [0, 1, 7],
|
||||
strikeMoneyness: 1.08,
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.78,
|
||||
nbbo_aggressive_ratio: 0.72,
|
||||
nbbo_aggressive_buy_ratio: 0.66,
|
||||
nbbo_aggressive_sell_ratio: 0.06,
|
||||
nbbo_inside_ratio: 0.1,
|
||||
execution_iv_shock: 0.19,
|
||||
venue_count: 4
|
||||
},
|
||||
conditions: ["ISO"]
|
||||
},
|
||||
{
|
||||
id: "noise",
|
||||
weight: 10,
|
||||
label: "neutral_noise",
|
||||
right: "either",
|
||||
countRange: [2, 4],
|
||||
sizeRange: [10, 200],
|
||||
targetNotionalRange: [500, 5000],
|
||||
priceTrend: "flat",
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.72,
|
||||
nbbo_aggressive_ratio: 0.24,
|
||||
nbbo_aggressive_buy_ratio: 0.24,
|
||||
nbbo_aggressive_sell_ratio: 0.2,
|
||||
nbbo_inside_ratio: 0.52,
|
||||
venue_count: 1
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
}
|
||||
];
|
||||
|
||||
const SMART_MONEY_TEMPLATE_SCENARIOS: Scenario[] = [
|
||||
{
|
||||
id: "institutional_directional",
|
||||
weight: 18,
|
||||
label: "institutional_directional",
|
||||
right: "C",
|
||||
countRange: [8, 10],
|
||||
sizeRange: [1600, 2400],
|
||||
targetNotionalRange: [170_000, 230_000],
|
||||
priceTrend: "up",
|
||||
expiryOffsets: [28, 45],
|
||||
strikeMoneyness: 1.01,
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.94,
|
||||
nbbo_aggressive_ratio: 0.86,
|
||||
nbbo_aggressive_buy_ratio: 0.82,
|
||||
nbbo_aggressive_sell_ratio: 0.04,
|
||||
nbbo_inside_ratio: 0.06,
|
||||
venue_count: 5
|
||||
},
|
||||
conditions: ["SWEEP"]
|
||||
},
|
||||
{
|
||||
id: "retail_whale",
|
||||
weight: 14,
|
||||
label: "retail_whale",
|
||||
right: "C",
|
||||
countRange: [9, 12],
|
||||
sizeRange: [450, 850],
|
||||
targetNotionalRange: [35_000, 75_000],
|
||||
priceTrend: "up",
|
||||
expiryOffsets: [1, 7],
|
||||
strikeMoneyness: 1.1,
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.82,
|
||||
nbbo_aggressive_ratio: 0.74,
|
||||
nbbo_aggressive_buy_ratio: 0.68,
|
||||
nbbo_aggressive_sell_ratio: 0.04,
|
||||
nbbo_inside_ratio: 0.08,
|
||||
execution_iv_shock: 0.19,
|
||||
venue_count: 4
|
||||
},
|
||||
conditions: ["ISO"]
|
||||
},
|
||||
{
|
||||
id: "event_driven",
|
||||
weight: 12,
|
||||
label: "event_driven",
|
||||
right: "C",
|
||||
countRange: [1, 2],
|
||||
sizeRange: [700, 1100],
|
||||
targetNotionalRange: [72_000, 88_000],
|
||||
priceTrend: "flat",
|
||||
expiryOffsets: [28, 45],
|
||||
strikeMoneyness: 1.0,
|
||||
flowFeatures: {
|
||||
corporate_event_ts_offset_days: 14,
|
||||
nbbo_coverage_ratio: 0.38,
|
||||
nbbo_aggressive_ratio: 0.32,
|
||||
nbbo_aggressive_buy_ratio: 0.3,
|
||||
nbbo_aggressive_sell_ratio: 0.08,
|
||||
nbbo_inside_ratio: 0.28,
|
||||
nbbo_spread_z: 0.12,
|
||||
venue_count: 2
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
},
|
||||
{
|
||||
id: "vol_seller",
|
||||
weight: 12,
|
||||
label: "vol_seller",
|
||||
right: "either",
|
||||
countRange: [4, 6],
|
||||
sizeRange: [1300, 2100],
|
||||
targetNotionalRange: [150_000, 210_000],
|
||||
priceTrend: "down",
|
||||
expiryOffsets: [28, 45],
|
||||
strikeMoneyness: 1.0,
|
||||
flowFeatures: {
|
||||
structure_type: "straddle",
|
||||
structure_legs: 2,
|
||||
structure_strikes: 1,
|
||||
structure_rights: "CP",
|
||||
conditions: "COMPLEX",
|
||||
nbbo_coverage_ratio: 0.9,
|
||||
nbbo_aggressive_ratio: 0.72,
|
||||
nbbo_aggressive_buy_ratio: 0.08,
|
||||
nbbo_aggressive_sell_ratio: 0.7,
|
||||
nbbo_inside_ratio: 0.1,
|
||||
same_size_leg_symmetry: 0.66,
|
||||
venue_count: 3
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
},
|
||||
{
|
||||
id: "arbitrage",
|
||||
weight: 12,
|
||||
label: "arbitrage",
|
||||
right: "either",
|
||||
countRange: [4, 6],
|
||||
sizeRange: [900, 1400],
|
||||
targetNotionalRange: [70_000, 115_000],
|
||||
priceTrend: "flat",
|
||||
expiryOffsets: [28, 45],
|
||||
strikeMoneyness: 1.0,
|
||||
flowFeatures: {
|
||||
structure_type: "vertical",
|
||||
structure_legs: 2,
|
||||
structure_strikes: 2,
|
||||
structure_rights: "CP",
|
||||
conditions: "COMPLEX",
|
||||
nbbo_coverage_ratio: 0.86,
|
||||
nbbo_aggressive_ratio: 0.4,
|
||||
nbbo_aggressive_buy_ratio: 0.42,
|
||||
nbbo_aggressive_sell_ratio: 0.38,
|
||||
nbbo_inside_ratio: 0.32,
|
||||
same_size_leg_symmetry: 0.92,
|
||||
venue_count: 3
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
},
|
||||
{
|
||||
id: "hedge_reactive",
|
||||
weight: 12,
|
||||
label: "hedge_reactive",
|
||||
right: "P",
|
||||
countRange: [1, 2],
|
||||
sizeRange: [2600, 3400],
|
||||
targetNotionalRange: [35_000, 50_000],
|
||||
priceTrend: "up",
|
||||
expiryOffsets: [0, 1],
|
||||
strikeMoneyness: 1.0,
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.86,
|
||||
nbbo_aggressive_ratio: 0.58,
|
||||
nbbo_aggressive_buy_ratio: 0.54,
|
||||
nbbo_aggressive_sell_ratio: 0.12,
|
||||
nbbo_inside_ratio: 0.16,
|
||||
underlying_move_bps: -72,
|
||||
venue_count: 3
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
},
|
||||
{
|
||||
id: "neutral_noise",
|
||||
weight: 20,
|
||||
label: "neutral_noise",
|
||||
right: "either",
|
||||
countRange: [1, 2],
|
||||
sizeRange: [10, 70],
|
||||
targetNotionalRange: [800, 7_000],
|
||||
priceTrend: "flat",
|
||||
expiryOffsets: [14, 28, 45, 60],
|
||||
strikeMoneyness: 1.02,
|
||||
flowFeatures: {
|
||||
nbbo_coverage_ratio: 0.78,
|
||||
nbbo_aggressive_ratio: 0.22,
|
||||
nbbo_aggressive_buy_ratio: 0.22,
|
||||
nbbo_aggressive_sell_ratio: 0.18,
|
||||
nbbo_inside_ratio: 0.58,
|
||||
venue_count: 1
|
||||
},
|
||||
conditions: ["FILL"]
|
||||
}
|
||||
];
|
||||
|
|
@ -292,6 +585,25 @@ const SYNTHETIC_PROFILES: Record<SyntheticMarketMode, SyntheticOptionsProfile> =
|
|||
}
|
||||
};
|
||||
|
||||
const SMART_MONEY_TEMPLATE_PROFILE: SyntheticOptionsProfile = {
|
||||
burstRunRange: [1, 1],
|
||||
scenarios: SMART_MONEY_TEMPLATE_SCENARIOS,
|
||||
pricePlacements: {
|
||||
...ACTIVE_PRICE_PLACEMENTS,
|
||||
institutional_directional: ACTIVE_PRICE_PLACEMENTS.bullish_sweep,
|
||||
retail_whale: ACTIVE_PRICE_PLACEMENTS.contract_spike,
|
||||
event_driven: REALISTIC_PRICE_PLACEMENTS.ask_lift,
|
||||
vol_seller: [
|
||||
{ value: "B", weight: 45 },
|
||||
{ value: "BB", weight: 35 },
|
||||
{ value: "MID", weight: 20 }
|
||||
],
|
||||
arbitrage: REALISTIC_PRICE_PLACEMENTS.mid_block,
|
||||
hedge_reactive: ACTIVE_PRICE_PLACEMENTS.bullish_sweep,
|
||||
neutral_noise: REALISTIC_PRICE_PLACEMENTS.noise
|
||||
}
|
||||
};
|
||||
|
||||
const pick = <T,>(items: T[], seed: number): T => {
|
||||
return items[Math.abs(seed) % items.length];
|
||||
};
|
||||
|
|
@ -414,14 +726,18 @@ const buildBurst = (burstIndex: number, now: number, profile: SyntheticOptionsPr
|
|||
const seed = symbolHash + burstIndex * 7;
|
||||
const scenario = pickWeighted(profile.scenarios, seed);
|
||||
const baseUnderlying = 30 + (symbolHash % 470);
|
||||
const expiryOffset = pick(EXPIRY_OFFSETS, symbolHash + burstIndex);
|
||||
const expiryOffset = pick(scenario.expiryOffsets ?? EXPIRY_OFFSETS, symbolHash + burstIndex);
|
||||
const expiry = formatExpiry(now, expiryOffset);
|
||||
const strikeStep = baseUnderlying >= 200 ? 10 : baseUnderlying >= 100 ? 5 : 2.5;
|
||||
const moneynessSteps = scenario.id === "noise" ? 5 : 2;
|
||||
const strikeOffset = pickInt(-moneynessSteps, moneynessSteps, symbolHash + burstIndex * 11);
|
||||
const templateStrike =
|
||||
scenario.strikeMoneyness !== undefined
|
||||
? Math.round((baseUnderlying * scenario.strikeMoneyness) / strikeStep) * strikeStep
|
||||
: null;
|
||||
const strike = Math.max(
|
||||
1,
|
||||
Math.round(baseUnderlying / strikeStep) * strikeStep + strikeOffset * strikeStep
|
||||
templateStrike ?? Math.round(baseUnderlying / strikeStep) * strikeStep + strikeOffset * strikeStep
|
||||
);
|
||||
const right =
|
||||
scenario.right === "either"
|
||||
|
|
@ -463,6 +779,8 @@ const buildBurst = (burstIndex: number, now: number, profile: SyntheticOptionsPr
|
|||
printCount,
|
||||
priceStep,
|
||||
scenarioId: scenario.id,
|
||||
label: scenario.label,
|
||||
flowFeatures: scenario.flowFeatures,
|
||||
seed
|
||||
};
|
||||
};
|
||||
|
|
@ -473,6 +791,68 @@ export const buildSyntheticBurstForTest = (
|
|||
mode: SyntheticMarketMode
|
||||
): Burst => buildBurst(burstIndex, now, SYNTHETIC_PROFILES[mode]);
|
||||
|
||||
export const listSyntheticSmartMoneyScenariosForTest = (): SyntheticSmartMoneyScenario[] =>
|
||||
SMART_MONEY_SCENARIO_IDS.map((id) => ({
|
||||
id,
|
||||
label: id,
|
||||
hiddenLabel: id
|
||||
}));
|
||||
|
||||
export const buildSyntheticSmartMoneyBurstForTest = (
|
||||
scenarioId: (typeof SMART_MONEY_SCENARIO_IDS)[number],
|
||||
now: number
|
||||
): Burst => {
|
||||
const scenarioIndex = SMART_MONEY_TEMPLATE_SCENARIOS.findIndex((scenario) => scenario.id === scenarioId);
|
||||
if (scenarioIndex < 0) {
|
||||
throw new Error(`Unknown synthetic smart-money scenario: ${scenarioId}`);
|
||||
}
|
||||
return buildBurst(scenarioIndex, now, {
|
||||
...SMART_MONEY_TEMPLATE_PROFILE,
|
||||
scenarios: [SMART_MONEY_TEMPLATE_SCENARIOS[scenarioIndex]]
|
||||
});
|
||||
};
|
||||
|
||||
export const buildSyntheticFlowPacketForTest = (
|
||||
scenarioId: (typeof SMART_MONEY_SCENARIO_IDS)[number],
|
||||
now: number
|
||||
): { packet: FlowPacket; hiddenLabel: SyntheticScenarioLabel } => {
|
||||
const burst = buildSyntheticSmartMoneyBurstForTest(scenarioId, now);
|
||||
const corporateEventOffset = Number(burst.flowFeatures.corporate_event_ts_offset_days ?? 0);
|
||||
const flowFeatures: FlowPacket["features"] = {
|
||||
option_contract_id: burst.contractId,
|
||||
underlying_id: burst.contractId.split("-")[0],
|
||||
underlying_mid: burst.underlying,
|
||||
count: burst.printCount,
|
||||
window_ms: Math.max(0, (burst.printCount - 1) * 45),
|
||||
total_size: burst.baseSize * burst.printCount,
|
||||
total_premium: Number((burst.basePrice * burst.baseSize * burst.printCount * OPTION_CONTRACT_MULTIPLIER).toFixed(2)),
|
||||
total_notional: Number((burst.underlying * burst.baseSize * burst.printCount * OPTION_CONTRACT_MULTIPLIER).toFixed(2)),
|
||||
first_price: burst.basePrice,
|
||||
last_price: Number((burst.basePrice * (1 + burst.priceStep * Math.max(0, burst.printCount - 1))).toFixed(2)),
|
||||
nbbo_missing_count: 0,
|
||||
nbbo_stale_count: 0,
|
||||
...burst.flowFeatures
|
||||
};
|
||||
delete flowFeatures.corporate_event_ts_offset_days;
|
||||
if (corporateEventOffset > 0) {
|
||||
flowFeatures.corporate_event_ts = now + corporateEventOffset * MS_PER_DAY;
|
||||
}
|
||||
|
||||
return {
|
||||
hiddenLabel: burst.label,
|
||||
packet: {
|
||||
source_ts: now,
|
||||
ingest_ts: now,
|
||||
seq: SMART_MONEY_SCENARIO_IDS.indexOf(scenarioId) + 1,
|
||||
trace_id: `synthetic-smart-money:${scenarioId}`,
|
||||
id: `synthetic-smart-money:${scenarioId}:${now}`,
|
||||
members: Array.from({ length: burst.printCount }, (_, index) => `${burst.contractId}:${index + 1}`),
|
||||
features: flowFeatures,
|
||||
join_quality: {}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const createSyntheticOptionsAdapter = (
|
||||
config: SyntheticOptionsAdapterConfig
|
||||
): OptionIngestAdapter => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { describe, expect, it } from "bun:test";
|
||||
import { buildSyntheticBurstForTest, updateSyntheticIvForTest } from "../src/adapters/synthetic";
|
||||
import type { OptionPrint } from "@islandflow/types";
|
||||
import { buildSmartMoneyEventFromPacket } from "../../compute/src/parent-events";
|
||||
import {
|
||||
buildSyntheticBurstForTest,
|
||||
buildSyntheticFlowPacketForTest,
|
||||
createSyntheticOptionsAdapter,
|
||||
listSyntheticSmartMoneyScenariosForTest,
|
||||
updateSyntheticIvForTest
|
||||
} from "../src/adapters/synthetic";
|
||||
|
||||
const totalBurstNotional = (burst: {
|
||||
basePrice: number;
|
||||
|
|
@ -87,3 +95,76 @@ describe("synthetic options IV model", () => {
|
|||
expect(state.iv).toBeLessThanOrEqual(2.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("synthetic smart-money scenarios", () => {
|
||||
it("provides deterministic labeled parent-event templates for all core profiles plus noise", () => {
|
||||
const scenarios = listSyntheticSmartMoneyScenariosForTest();
|
||||
|
||||
expect(scenarios.map((scenario) => scenario.id)).toEqual([
|
||||
"institutional_directional",
|
||||
"retail_whale",
|
||||
"event_driven",
|
||||
"vol_seller",
|
||||
"arbitrage",
|
||||
"hedge_reactive",
|
||||
"neutral_noise"
|
||||
]);
|
||||
});
|
||||
|
||||
it("scores each labeled scenario as its intended primary profile", () => {
|
||||
const now = Date.parse("2026-01-02T15:00:00Z");
|
||||
const scenarios = listSyntheticSmartMoneyScenariosForTest().filter(
|
||||
(scenario) => scenario.hiddenLabel !== "neutral_noise"
|
||||
);
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const { packet, hiddenLabel } = buildSyntheticFlowPacketForTest(scenario.id, now);
|
||||
const event = buildSmartMoneyEventFromPacket(packet);
|
||||
const winningScore = event.profile_scores[0];
|
||||
const nearbyWrongScores = event.profile_scores.filter(
|
||||
(score) => score.profile_id !== hiddenLabel && score.probability >= 0.5
|
||||
);
|
||||
|
||||
expect(event.abstained, scenario.id).toBe(false);
|
||||
expect(event.primary_profile_id, scenario.id).toBe(hiddenLabel);
|
||||
expect(winningScore?.profile_id, scenario.id).toBe(hiddenLabel);
|
||||
expect(winningScore?.probability ?? 0, scenario.id).toBeGreaterThanOrEqual(0.5);
|
||||
expect(nearbyWrongScores, scenario.id).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps neutral background noise below the emission threshold", () => {
|
||||
const { packet } = buildSyntheticFlowPacketForTest(
|
||||
"neutral_noise",
|
||||
Date.parse("2026-01-02T15:00:00Z")
|
||||
);
|
||||
|
||||
const event = buildSmartMoneyEventFromPacket(packet);
|
||||
|
||||
expect(event.abstained).toBe(true);
|
||||
expect(event.primary_profile_id).toBeNull();
|
||||
expect(event.profile_scores[0]?.probability ?? 1).toBeLessThan(0.42);
|
||||
});
|
||||
|
||||
it("does not expose hidden labels on emitted option prints", async () => {
|
||||
const adapter = createSyntheticOptionsAdapter({
|
||||
emitIntervalMs: 1,
|
||||
mode: "active"
|
||||
});
|
||||
const trades: OptionPrint[] = [];
|
||||
const stop = adapter.start({
|
||||
onTrade: (trade) => {
|
||||
trades.push(trade);
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
stop();
|
||||
|
||||
expect(trades.length).toBeGreaterThan(0);
|
||||
for (const trade of trades) {
|
||||
expect("hiddenLabel" in trade).toBe(false);
|
||||
expect("label" in trade).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue