Rebuild synthetic smart-money scenarios

This commit is contained in:
dirtydishes 2026-05-05 01:29:39 -04:00
parent 6b794ec7ac
commit 86661df7ae
4 changed files with 468 additions and 7 deletions

View file

@ -6,7 +6,7 @@
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-b6d","title":"Finish smart-money event-calendar enrichment","description":"Finish the smart-money event-calendar provider layer in services/refdata and connect days-to-event / expiry-after-event enrichment into compute using timestamp-available data only.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:26Z","created_by":"dirtydishes","updated_at":"2026-05-04T23:21:09Z","started_at":"2026-05-04T23:18:29Z","closed_at":"2026-05-04T23:21:09Z","close_reason":"Completed event-calendar provider and compute enrichment","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-e60","title":"Add smart-money replay evaluation harness","description":"Add replay-style live-vs-batch consistency tests plus evaluation utilities for parent-event precision/recall, calibration, abstention rate, and economic sanity checks.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:25Z","created_by":"dirtydishes","updated_at":"2026-05-04T21:35:25Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-020","title":"Rebuild synthetic smart-money scenarios","description":"Rework services/ingest-options synthetic generation around labeled parent-event templates for the six core smart-money profiles plus neutral background noise, with deterministic test/demo modes and hidden labels for tests.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:24Z","created_by":"dirtydishes","updated_at":"2026-05-04T21:35:24Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-020","title":"Rebuild synthetic smart-money scenarios","description":"Rework services/ingest-options synthetic generation around labeled parent-event templates for the six core smart-money profiles plus neutral background noise, with deterministic test/demo modes and hidden labels for tests.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:24Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:29:27Z","started_at":"2026-05-05T05:25:39Z","closed_at":"2026-05-05T05:29:27Z","close_reason":"Completed Phase 5 synthetic smart-money scenario rebuild","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-04T21:35:23Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -36,9 +36,9 @@ Acceptance: missing event-calendar fields produce neutral `null` feature values
Acceptance: abstained events do not emit legacy classifier hits.
### Phase 5: Synthetic Market Redesign
- [ ] Rework synthetic options adapter around labeled parent-event templates.
- [ ] Add deterministic scenario families for all six profiles.
- [ ] Add test/demo operating modes with hidden labels.
- [x] Rework synthetic options adapter around labeled parent-event templates.
- [x] Add deterministic scenario families for all six profiles.
- [x] Add test/demo operating modes with hidden labels.
Acceptance: scenario tests assert intended profile wins and wrong nearby profiles remain below threshold.

View file

@ -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 => {

View file

@ -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);
}
});
});