diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 27abff1..b6f4b0b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,5 @@ +{"_type":"issue","id":"islandflow-9nd","title":"Hosted synthetic tape redesign with internal control surface","description":"Implement hosted synthetic market redesign with shared deterministic regime engine, internal JetStream KV control plane, ingest coupling across options and equities, and an internal bottom-right synthetic-control drawer with Next proxy routes. Preserve the six public smart-money categories while adding hidden subtype families, soft coverage accounting, and backend-only admin endpoints.\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-14T01:25:02Z","created_by":"dirtydishes","updated_at":"2026-05-14T02:10:03Z","started_at":"2026-05-14T01:25:09Z","closed_at":"2026-05-14T02:10:03Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-9dz","title":"Tune synthetic smart-money scenario coverage","description":"Redesign synthetic smart-money option prints so the emitted scenarios trigger each classifier category more consistently while staying directionally plausible. Focus on scenario mix, DTE/moneyness, price placement, and event/structure context so the Electron demo reliably shows institutional directional, retail whale, event-driven, vol seller, arbitrage, and hedge reactive hits.\n","status":"in_progress","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-13T21:36:37Z","created_by":"dirtydishes","updated_at":"2026-05-13T21:36:41Z","started_at":"2026-05-13T21:36:41Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-zuf","title":"Fix Home to Tape tab navigation freeze","description":"Home-to-Tape navigation becomes unresponsive because TerminalAppShell enters a live-mode rerender loop. The pinned-evidence prune effect writes new Map instances even when contents are unchanged, which can retrigger state updates indefinitely on the Home route where alert evidence prefetch is active. Make pruning idempotent and add regression coverage.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-13T15:05:56Z","created_by":"dirtydishes","updated_at":"2026-05-13T15:08:01Z","started_at":"2026-05-13T15:06:06Z","closed_at":"2026-05-13T15:08:01Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-9ug","title":"Electron desktop shell for hosted Islandflow","description":"Build a macOS-first Electron desktop shell workspace that loads hosted Islandflow in a locked-down BrowserWindow, adds Bun-first dev/package scripts, documents the workflow, and preserves the existing remote API/WS contract.\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-13T13:11:40Z","created_by":"dirtydishes","updated_at":"2026-05-13T13:20:57Z","started_at":"2026-05-13T13:12:03Z","closed_at":"2026-05-13T13:20:57Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-sh1","title":"Fix live websocket stale lag and reconnect loop","description":"Investigate and fix API live consumer lag causing stale timestamps, feed-behind status, and reconnect loops. Optimize live cache persistence path, add lag telemetry/alerts, and validate in runtime.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T17:04:34Z","created_by":"dirtydishes","updated_at":"2026-05-04T17:09:44Z","started_at":"2026-05-04T17:04:38Z","closed_at":"2026-05-04T17:09:44Z","close_reason":"Completed: optimized live cache persistence path, added lag telemetry, deployed api via docker compose on di, verified ws freshness and low hotFeedLagMs","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/apps/web/app/api/admin/synthetic/control/route.ts b/apps/web/app/api/admin/synthetic/control/route.ts new file mode 100644 index 0000000..09f5629 --- /dev/null +++ b/apps/web/app/api/admin/synthetic/control/route.ts @@ -0,0 +1,19 @@ +import { proxySyntheticAdminRequest } from "../shared"; + +export const dynamic = "force-dynamic"; + +export async function GET(): Promise { + return proxySyntheticAdminRequest("/admin/synthetic/control", { + method: "GET" + }); +} + +export async function PUT(req: Request): Promise { + return proxySyntheticAdminRequest( + "/admin/synthetic/control", + { + method: "PUT", + body: await req.text() + } + ); +} diff --git a/apps/web/app/api/admin/synthetic/routes.test.ts b/apps/web/app/api/admin/synthetic/routes.test.ts new file mode 100644 index 0000000..0372d90 --- /dev/null +++ b/apps/web/app/api/admin/synthetic/routes.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; +import { + getSyntheticAdminProxyConfig, + isSyntheticAdminFeatureEnabled +} from "./shared"; + +const originalFetch = globalThis.fetch; + +describe("synthetic admin proxy helpers", () => { + beforeEach(() => { + process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN = "1"; + process.env.NEXT_PUBLIC_API_URL = "http://127.0.0.1:4000"; + process.env.SYNTHETIC_ADMIN_TOKEN = "secret-token"; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + it("gates visibility on the public env flag", () => { + expect(isSyntheticAdminFeatureEnabled("1")).toBe(true); + expect(isSyntheticAdminFeatureEnabled("0")).toBe(false); + }); + + it("reads the proxy config from server env only", () => { + expect(getSyntheticAdminProxyConfig()).toEqual({ + apiBaseUrl: "http://127.0.0.1:4000", + token: "secret-token" + }); + }); + + it("proxies status requests with the backend admin token", async () => { + const fetchMock = mock(async (input: string | URL, init?: RequestInit) => { + expect(String(input)).toBe("http://127.0.0.1:4000/admin/synthetic/status"); + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer secret-token"); + return new Response(JSON.stringify({ enabled: true }), { + status: 200, + headers: { + "content-type": "application/json" + } + }); + }); + globalThis.fetch = fetchMock as typeof fetch; + const route = await import("./status/route"); + + const response = await route.GET(); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ enabled: true }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("returns 404 from proxy routes when the internal UI flag is off", async () => { + process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN = "0"; + const route = await import("./control/route"); + + const response = await route.GET(); + + expect(response.status).toBe(404); + }); +}); diff --git a/apps/web/app/api/admin/synthetic/shared.ts b/apps/web/app/api/admin/synthetic/shared.ts new file mode 100644 index 0000000..cc75fff --- /dev/null +++ b/apps/web/app/api/admin/synthetic/shared.ts @@ -0,0 +1,63 @@ +const jsonResponse = (body: unknown, status = 200): Response => { + return new Response(JSON.stringify(body), { + status, + headers: { + "content-type": "application/json" + } + }); +}; + +export const isSyntheticAdminFeatureEnabled = ( + value = process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN +): boolean => value === "1"; + +export const getSyntheticAdminProxyConfig = ( + env: Record = process.env +): { apiBaseUrl: string; token: string } | null => { + const apiBaseUrl = env.NEXT_PUBLIC_API_URL?.trim(); + const token = env.SYNTHETIC_ADMIN_TOKEN?.trim(); + if (!apiBaseUrl || !token) { + return null; + } + return { apiBaseUrl, token }; +}; + +export const proxySyntheticAdminRequest = async ( + path: string, + init: RequestInit = {}, + env: Record = process.env +): Promise => { + if (!isSyntheticAdminFeatureEnabled(env.NEXT_PUBLIC_SYNTHETIC_ADMIN)) { + return jsonResponse({ error: "not found" }, 404); + } + + const config = getSyntheticAdminProxyConfig(env); + if (!config) { + return jsonResponse( + { + error: "synthetic admin proxy misconfigured" + }, + 500 + ); + } + + const url = new URL(path, config.apiBaseUrl); + const headers = new Headers(init.headers); + headers.set("authorization", `Bearer ${config.token}`); + if (!headers.has("content-type") && init.body) { + headers.set("content-type", "application/json"); + } + + const response = await fetch(url.toString(), { + ...init, + cache: "no-store", + headers + }); + + return new Response(response.body, { + status: response.status, + headers: { + "content-type": response.headers.get("content-type") ?? "application/json" + } + }); +}; diff --git a/apps/web/app/api/admin/synthetic/status/route.ts b/apps/web/app/api/admin/synthetic/status/route.ts new file mode 100644 index 0000000..7477485 --- /dev/null +++ b/apps/web/app/api/admin/synthetic/status/route.ts @@ -0,0 +1,9 @@ +import { proxySyntheticAdminRequest } from "../shared"; + +export const dynamic = "force-dynamic"; + +export async function GET(): Promise { + return proxySyntheticAdminRequest("/admin/synthetic/status", { + method: "GET" + }); +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 8cf07a3..777505b 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1507,6 +1507,196 @@ h3 { z-index: 40; } +.synthetic-control-gear { + position: fixed; + right: 22px; + bottom: 22px; + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(245, 166, 35, 0.24); + border-radius: 12px; + background: rgba(9, 13, 18, 0.96); + color: var(--accent); + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.38); + z-index: 45; + transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease; +} + +.synthetic-control-gear:hover, +.synthetic-control-gear.is-open { + transform: translateY(-1px); + border-color: rgba(245, 166, 35, 0.4); + background: rgba(12, 18, 24, 0.98); +} + +.synthetic-control-gear-mark { + display: inline-flex; + font-size: 1.05rem; + line-height: 1; + transform: rotate(45deg); +} + +.synthetic-control-drawer { + position: fixed; + top: 84px; + right: 0; + bottom: 0; + width: min(388px, calc(100vw - 20px)); + padding: 18px 18px 24px; + display: grid; + align-content: start; + gap: 16px; + overflow: auto; + border-left: 1px solid rgba(245, 166, 35, 0.18); + background: + linear-gradient(180deg, rgba(245, 166, 35, 0.04), transparent 18%), + rgba(6, 9, 13, 0.98); + box-shadow: -18px 0 50px rgba(0, 0, 0, 0.34); + z-index: 42; +} + +.synthetic-control-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.synthetic-control-header h3 { + margin: 0; + font-size: 1rem; + letter-spacing: 0.04em; +} + +.synthetic-control-kicker { + margin: 0 0 6px; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.16em; + font-size: 0.64rem; +} + +.synthetic-control-section { + display: grid; + gap: 10px; + padding: 14px 14px 0; + border-top: 1px solid var(--border); +} + +.synthetic-control-section-head { + display: flex; + justify-content: space-between; + gap: 12px; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.68rem; +} + +.synthetic-control-select select, +.synthetic-segment, +.synthetic-control-toggle { + font: inherit; +} + +.synthetic-control-select select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); + color: var(--text); +} + +.synthetic-control-toggle { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--text-dim); +} + +.synthetic-control-toggle input { + accent-color: var(--accent); +} + +.synthetic-segment-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.synthetic-segment { + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.02); + color: var(--text-dim); +} + +.synthetic-segment.is-active { + border-color: rgba(245, 166, 35, 0.44); + background: rgba(245, 166, 35, 0.12); + color: var(--text); +} + +.synthetic-profile-grid, +.synthetic-hit-list { + display: grid; + gap: 12px; +} + +.synthetic-profile-row, +.synthetic-hit-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.synthetic-profile-row > span, +.synthetic-hit-row > span, +.synthetic-status-grid span { + color: var(--text-dim); + font-size: 0.84rem; +} + +.synthetic-status-grid { + display: grid; + gap: 10px; +} + +.synthetic-status-grid strong, +.synthetic-hit-row strong { + font-family: var(--font-mono), monospace; + font-size: 0.86rem; +} + +.synthetic-control-disabled { + display: grid; + gap: 8px; + padding: 14px 14px 0; + border-top: 1px solid var(--border); +} + +.synthetic-control-disabled p, +.synthetic-control-disabled span { + margin: 0; +} + +.synthetic-control-disabled-label { + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.68rem; +} + +.synthetic-control-error { + color: var(--red); +} + .drawer-header { display: flex; align-items: flex-start; @@ -1732,4 +1922,19 @@ h3 { max-height: none; margin-top: 14px; } + + .synthetic-control-gear { + right: 14px; + bottom: 14px; + } + + .synthetic-control-drawer { + top: auto; + left: 14px; + right: 14px; + bottom: 68px; + width: auto; + border: 1px solid rgba(245, 166, 35, 0.16); + border-radius: 14px; + } } diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 91169a7..20647ca 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -28,6 +28,7 @@ import { mergeNewestWithOverflow, normalizeAlertSeverity, nextFlowFilterPopoverState, + isSyntheticAdminVisible, prunePinnedEntries, projectPausableTapeState, reducePausableTapeData, @@ -407,6 +408,13 @@ describe("terminal navigation", () => { }); }); +describe("synthetic admin visibility", () => { + it("shows the internal control rail only when the public admin flag is enabled", () => { + expect(isSyntheticAdminVisible("1")).toBe(true); + expect(isSyntheticAdminVisible("0")).toBe(false); + }); +}); + describe("live tape pausable helpers", () => { it("queues new items while paused and flushes them on resume", () => { let state = reducePausableTapeData( diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 352295a..e4d496e 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -39,7 +39,11 @@ import type { OptionNBBO, OptionPrint, SmartMoneyEvent, - SmartMoneyProfileId + SmartMoneyProfileId, + SyntheticControlState, + SyntheticCoverageWindowMinutes, + SyntheticDerivedStatus, + SyntheticProfileWeightValue } from "@islandflow/types"; import { getSubscriptionKey as getLiveSubscriptionKey, @@ -988,6 +992,96 @@ const buildApiUrl = (path: string): string => { return `${httpProtocol}://${host}${path}`; }; +export const isSyntheticAdminVisible = ( + value = process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN +): boolean => value === "1"; + +type SyntheticAdminStatusResponse = { + enabled: boolean; + backend_mode: "synthetic" | "mixed" | "live"; + adapters: { + options: string; + equities: string; + }; + control: SyntheticControlState | null; + derived: SyntheticDerivedStatus | null; + disabled_reason?: string; +}; + +type SyntheticAdminControlResponse = { + control: SyntheticControlState; + derived?: SyntheticDerivedStatus | null; +}; + +const SYNTHETIC_ADMIN_PROXY_PATHS = { + status: "/api/admin/synthetic/status", + control: "/api/admin/synthetic/control" +} as const; + +const SYNTHETIC_PROFILE_ORDER: Array = [ + "institutional_directional", + "retail_whale", + "event_driven", + "vol_seller", + "arbitrage", + "hedge_reactive" +]; + +const SYNTHETIC_PROFILE_LABELS: Record< + keyof SyntheticControlState["profile_weights"], + string +> = { + institutional_directional: "Institutional Directional", + retail_whale: "Retail Whale", + event_driven: "Event Driven", + vol_seller: "Vol Seller", + arbitrage: "Arbitrage", + hedge_reactive: "Hedge Reactive" +}; + +const SYNTHETIC_PRESET_LABELS: Record = { + balanced_demo: "Balanced Demo", + event_day: "Event Day", + dealer_day: "Dealer Day", + retail_chase: "Retail Chase", + quiet_range: "Quiet Range" +}; + +const buildDefaultSyntheticControl = (): SyntheticControlState => ({ + preset_id: "balanced_demo", + coverage_assist: true, + coverage_window_minutes: 20, + shared_seed: 11, + profile_weights: { + institutional_directional: 1.0, + retail_whale: 1.0, + event_driven: 1.0, + vol_seller: 1.0, + arbitrage: 1.0, + hedge_reactive: 1.0 + }, + updated_at: 0, + updated_by: "internal-ui" +}); + +type SyntheticControlPatch = Omit, "profile_weights"> & { + profile_weights?: Partial; +}; + +const createSyntheticControlDraft = ( + current: SyntheticControlState, + patch: SyntheticControlPatch +): SyntheticControlState => ({ + ...current, + ...patch, + profile_weights: { + ...current.profile_weights, + ...(patch.profile_weights ?? {}) + }, + updated_at: Date.now(), + updated_by: "internal-ui" +}); + const formatPrice = (price: number): string => { if (!Number.isFinite(price)) { return "0.00"; @@ -7926,6 +8020,331 @@ const ReplayConsole = memo(({ state }: { state: TerminalState }) => { ); }); +function SyntheticControlDock() { + const visible = isSyntheticAdminVisible(); + const [open, setOpen] = useState(false); + const [status, setStatus] = useState(null); + const [draft, setDraft] = useState(null); + const [saved, setSaved] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const dirtyRef = useRef(false); + const savedRef = useRef(null); + + useEffect(() => { + if (!visible) { + return; + } + + let cancelled = false; + const load = async () => { + try { + const response = await fetch(SYNTHETIC_ADMIN_PROXY_PATHS.status, { + cache: "no-store" + }); + if (cancelled) { + return; + } + if (response.status === 404) { + setStatus({ + enabled: false, + backend_mode: "live", + adapters: { options: "unknown", equities: "unknown" }, + control: null, + derived: null, + disabled_reason: "Synthetic admin backend is disabled." + }); + setLoading(false); + return; + } + const nextStatus = (await response.json()) as SyntheticAdminStatusResponse; + setStatus(nextStatus); + if (!dirtyRef.current) { + const nextControl = nextStatus.control ?? buildDefaultSyntheticControl(); + setDraft(nextControl); + setSaved(nextControl); + savedRef.current = nextControl; + } + } catch (loadError) { + if (!cancelled) { + setError(loadError instanceof Error ? loadError.message : String(loadError)); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + void load(); + const timer = setInterval(() => { + void load(); + }, 5_000); + + return () => { + cancelled = true; + clearInterval(timer); + }; + }, [visible]); + + useEffect(() => { + if (!visible || !status?.enabled || !draft || !dirtyRef.current) { + return; + } + + const timeout = setTimeout(() => { + const nextDraft = draft; + setSaving(true); + setError(null); + void fetch(SYNTHETIC_ADMIN_PROXY_PATHS.control, { + method: "PUT", + headers: { + "content-type": "application/json" + }, + body: JSON.stringify(nextDraft) + }) + .then(async (response) => { + if (!response.ok) { + const body = await response.json().catch(() => null); + throw new Error(body?.detail ?? body?.error ?? "Synthetic control update failed"); + } + return (await response.json()) as SyntheticAdminControlResponse; + }) + .then((payload) => { + dirtyRef.current = false; + savedRef.current = payload.control; + setSaved(payload.control); + setDraft(payload.control); + setStatus((current) => + current + ? { + ...current, + control: payload.control, + derived: payload.derived ?? current.derived + } + : current + ); + }) + .catch((updateError) => { + dirtyRef.current = false; + setError(updateError instanceof Error ? updateError.message : String(updateError)); + setDraft(savedRef.current); + }) + .finally(() => { + setSaving(false); + }); + }, 250); + + return () => { + clearTimeout(timeout); + }; + }, [draft, status?.enabled, visible]); + + if (!visible) { + return null; + } + + const currentControl = draft ?? saved ?? buildDefaultSyntheticControl(); + const disabled = !status?.enabled; + const derived = status?.derived; + + const updateControl = ( + patch: SyntheticControlPatch + ) => { + dirtyRef.current = true; + setDraft((current) => + createSyntheticControlDraft(current ?? buildDefaultSyntheticControl(), patch) + ); + }; + + const updateProfileWeight = ( + profileId: keyof SyntheticControlState["profile_weights"], + value: SyntheticProfileWeightValue + ) => { + updateControl({ + profile_weights: { + [profileId]: value + } as Partial + }); + }; + + return ( + <> + + + {open ? ( + + ) : null} + + ); +} + export function TerminalAppShell({ children }: { children: ReactNode }) { const state = useTerminalState(); const pathname = usePathname(); @@ -8003,6 +8422,8 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
{children}
+ + {state.selectedAlert ? ( (); + +const decodeSyntheticControlEntry = ( + entry: KvEntry | null | undefined +): SyntheticControlState => { + if (!entry || entry.operation !== "PUT") { + return DEFAULT_SYNTHETIC_CONTROL_STATE; + } + return SyntheticControlStateSchema.parse(entry.json()); +}; + +export const openSyntheticControlKv = async ( + js: JetStreamClient +): Promise => { + return js.views.kv(SYNTHETIC_CONTROL_BUCKET, { + description: "Hosted synthetic market internal control state", + history: 8 + }); +}; + +export const readSyntheticControlState = async ( + kv: KV +): Promise => { + return decodeSyntheticControlEntry( + await kv.get(SYNTHETIC_CONTROL_GLOBAL_KEY) + ); +}; + +export const ensureSyntheticControlState = async ( + kv: KV +): Promise => { + const current = await kv.get(SYNTHETIC_CONTROL_GLOBAL_KEY); + if (current && current.operation === "PUT") { + return SyntheticControlStateSchema.parse(current.json()); + } + + await kv.put( + SYNTHETIC_CONTROL_GLOBAL_KEY, + codec.encode(DEFAULT_SYNTHETIC_CONTROL_STATE) + ); + return DEFAULT_SYNTHETIC_CONTROL_STATE; +}; + +export const writeSyntheticControlState = async ( + kv: KV, + control: Partial +): Promise => { + const normalized = normalizeSyntheticControlState(control); + await kv.put( + SYNTHETIC_CONTROL_GLOBAL_KEY, + codec.encode(normalized) + ); + return normalized; +}; + +export const watchSyntheticControlState = async ( + kv: KV, + onUpdate: (control: SyntheticControlState) => void, + onError?: (error: unknown) => void +): Promise<() => Promise> => { + const iterator = await kv.watch({ + key: SYNTHETIC_CONTROL_GLOBAL_KEY, + ignoreDeletes: true + }); + let stopped = false; + const task = (async () => { + try { + for await (const entry of iterator) { + if (stopped || entry.operation !== "PUT") { + continue; + } + onUpdate(SyntheticControlStateSchema.parse(entry.json())); + } + } catch (error) { + if (!stopped) { + onError?.(error); + } + } + })(); + + return async () => { + if (stopped) { + return; + } + stopped = true; + iterator.stop(); + await task; + }; +}; diff --git a/packages/types/src/synthetic-market.ts b/packages/types/src/synthetic-market.ts new file mode 100644 index 0000000..ea30c86 --- /dev/null +++ b/packages/types/src/synthetic-market.ts @@ -0,0 +1,834 @@ +import { z } from "zod"; +import type { SmartMoneyProfileId } from "./events"; +import type { SyntheticMarketMode } from "./options-flow"; +import { SP500_SYMBOLS } from "./sp500"; + +const SYNTHETIC_PROFILE_WEIGHT_VALUES = [0.6, 1.0, 1.6] as const; +const SYNTHETIC_COVERAGE_WINDOW_VALUES = [10, 20, 30] as const; +const SYNTHETIC_SYMBOLS = ["SPY", ...(SP500_SYMBOLS as readonly string[])]; +const EVENT_SYMBOL_POOL = [ + "AAPL", + "MSFT", + "NVDA", + "META", + "AMZN", + "TSLA", + "GOOGL", + "NFLX", + "AMD", + "AVGO" +] as const; +const SMART_MONEY_PROFILE_IDS = [ + "institutional_directional", + "retail_whale", + "event_driven", + "vol_seller", + "arbitrage", + "hedge_reactive" +] as const satisfies readonly SmartMoneyProfileId[]; +const SYNTHETIC_SCENARIO_FAMILY_IDS = [ + ...SMART_MONEY_PROFILE_IDS, + "neutral_noise" +] as const; +const REGIME_IDS = [ + "trend_up", + "trend_down", + "mean_revert", + "retail_chase", + "event_ramp", + "dealer_gamma", + "arb_calm" +] as const; + +export const SyntheticControlPresetIdSchema = z.enum([ + "balanced_demo", + "event_day", + "dealer_day", + "retail_chase", + "quiet_range" +]); +export type SyntheticControlPresetId = z.infer; + +export const SyntheticCoverageWindowMinutesSchema = z.union([ + z.literal(10), + z.literal(20), + z.literal(30) +]); +export type SyntheticCoverageWindowMinutes = z.infer< + typeof SyntheticCoverageWindowMinutesSchema +>; + +export const SyntheticProfileWeightValueSchema = z.union([ + z.literal(0.6), + z.literal(1.0), + z.literal(1.6) +]); +export type SyntheticProfileWeightValue = z.infer< + typeof SyntheticProfileWeightValueSchema +>; + +export const SyntheticProfileWeightMapSchema = z + .object({ + institutional_directional: SyntheticProfileWeightValueSchema, + retail_whale: SyntheticProfileWeightValueSchema, + event_driven: SyntheticProfileWeightValueSchema, + vol_seller: SyntheticProfileWeightValueSchema, + arbitrage: SyntheticProfileWeightValueSchema, + hedge_reactive: SyntheticProfileWeightValueSchema + }) + .strict(); +export type SyntheticProfileWeightMap = z.infer< + typeof SyntheticProfileWeightMapSchema +>; + +export const SyntheticControlStateSchema = z + .object({ + preset_id: SyntheticControlPresetIdSchema, + coverage_assist: z.boolean(), + coverage_window_minutes: SyntheticCoverageWindowMinutesSchema, + shared_seed: z.number().int(), + profile_weights: SyntheticProfileWeightMapSchema, + updated_at: z.number().int().nonnegative(), + updated_by: z.string().trim().min(1) + }) + .strict(); +export type SyntheticControlState = z.infer; + +export const SyntheticSessionPhaseSchema = z.enum([ + "open", + "midday", + "power_hour", + "after_event" +]); +export type SyntheticSessionPhase = z.infer; + +export const SyntheticRegimeSchema = z.enum(REGIME_IDS); +export type SyntheticRegime = z.infer; + +export const SyntheticScenarioFamilyIdSchema = z.enum( + SYNTHETIC_SCENARIO_FAMILY_IDS +); +export type SyntheticScenarioFamilyId = z.infer< + typeof SyntheticScenarioFamilyIdSchema +>; + +export const SyntheticCoverageConfigSchema = z + .object({ + coverage_assist: z.boolean(), + coverage_window_minutes: SyntheticCoverageWindowMinutesSchema + }) + .strict(); +export type SyntheticCoverageConfig = z.infer< + typeof SyntheticCoverageConfigSchema +>; + +export const SyntheticDerivedStatusSchema = z + .object({ + session_phase: SyntheticSessionPhaseSchema, + regime: SyntheticRegimeSchema, + focus_symbols: z.array(z.string()), + profile_hit_counts: z.record(z.number().int().nonnegative()), + coverage_window_minutes: SyntheticCoverageWindowMinutesSchema + }) + .strict(); +export type SyntheticDerivedStatus = z.infer< + typeof SyntheticDerivedStatusSchema +>; + +export type SyntheticSessionState = { + session_phase: SyntheticSessionPhase; + regime: SyntheticRegime; + volatility_level: number; + liquidity_level: number; + quote_cleanliness: number; + focus_symbols: string[]; + event_symbols: string[]; + seed_bucket: number; +}; + +export type SyntheticUnderlyingState = { + mid: number; + bid: number; + ask: number; + spread: number; + driftBps: number; + shockBps: number; + sessionVolatility: number; + liquiditySkew: number; + quoteCleanliness: number; + clusteringScore: number; + offExchangeBias: number; +}; + +export type SyntheticScenarioWeightMap = Record< + SyntheticScenarioFamilyId, + number +>; + +export type SyntheticCoverageState = { + profile_hit_counts: Record; +}; + +export type SyntheticBurstPulse = { + active: boolean; + intensity: number; + focusSymbols: string[]; + bucket: number; +}; + +const DEFAULT_PROFILE_WEIGHTS: SyntheticProfileWeightMap = { + institutional_directional: 1.0, + retail_whale: 1.0, + event_driven: 1.0, + vol_seller: 1.0, + arbitrage: 1.0, + hedge_reactive: 1.0 +}; + +export const DEFAULT_SYNTHETIC_CONTROL_STATE: SyntheticControlState = { + preset_id: "balanced_demo", + coverage_assist: true, + coverage_window_minutes: 20, + shared_seed: 11, + profile_weights: DEFAULT_PROFILE_WEIGHTS, + updated_at: 0, + updated_by: "system" +}; + +const PRESET_REGIME_BIAS: Record< + SyntheticControlPresetId, + Record +> = { + balanced_demo: { + trend_up: 1.0, + trend_down: 0.95, + mean_revert: 1.05, + retail_chase: 0.95, + event_ramp: 0.85, + dealer_gamma: 0.95, + arb_calm: 0.95 + }, + event_day: { + trend_up: 0.9, + trend_down: 0.9, + mean_revert: 0.75, + retail_chase: 0.95, + event_ramp: 1.9, + dealer_gamma: 1.0, + arb_calm: 0.55 + }, + dealer_day: { + trend_up: 0.85, + trend_down: 0.85, + mean_revert: 0.9, + retail_chase: 0.85, + event_ramp: 0.7, + dealer_gamma: 1.95, + arb_calm: 0.8 + }, + retail_chase: { + trend_up: 1.1, + trend_down: 0.7, + mean_revert: 0.6, + retail_chase: 2.0, + event_ramp: 0.95, + dealer_gamma: 0.95, + arb_calm: 0.45 + }, + quiet_range: { + trend_up: 0.7, + trend_down: 0.7, + mean_revert: 1.35, + retail_chase: 0.45, + event_ramp: 0.5, + dealer_gamma: 0.75, + arb_calm: 1.8 + } +}; + +const PRESET_ACTIVITY_BIAS: Record< + SyntheticControlPresetId, + { focusCount: number; eventCount: number; amplitude: number } +> = { + balanced_demo: { focusCount: 3, eventCount: 2, amplitude: 1.0 }, + event_day: { focusCount: 4, eventCount: 3, amplitude: 1.28 }, + dealer_day: { focusCount: 3, eventCount: 1, amplitude: 1.12 }, + retail_chase: { focusCount: 4, eventCount: 1, amplitude: 1.25 }, + quiet_range: { focusCount: 2, eventCount: 1, amplitude: 0.72 } +}; + +const REGIME_PROFILE_BIAS: Record< + SyntheticRegime, + SyntheticScenarioWeightMap +> = { + trend_up: { + institutional_directional: 1.35, + retail_whale: 1.05, + event_driven: 0.9, + vol_seller: 0.78, + arbitrage: 0.72, + hedge_reactive: 0.82, + neutral_noise: 0.82 + }, + trend_down: { + institutional_directional: 1.2, + retail_whale: 0.82, + event_driven: 0.88, + vol_seller: 0.8, + arbitrage: 0.78, + hedge_reactive: 1.22, + neutral_noise: 0.85 + }, + mean_revert: { + institutional_directional: 0.92, + retail_whale: 0.78, + event_driven: 0.8, + vol_seller: 1.18, + arbitrage: 1.28, + hedge_reactive: 0.92, + neutral_noise: 1.2 + }, + retail_chase: { + institutional_directional: 1.04, + retail_whale: 1.72, + event_driven: 0.9, + vol_seller: 0.7, + arbitrage: 0.58, + hedge_reactive: 0.98, + neutral_noise: 0.72 + }, + event_ramp: { + institutional_directional: 1.08, + retail_whale: 0.96, + event_driven: 1.95, + vol_seller: 0.74, + arbitrage: 0.62, + hedge_reactive: 1.04, + neutral_noise: 0.58 + }, + dealer_gamma: { + institutional_directional: 0.94, + retail_whale: 1.02, + event_driven: 0.78, + vol_seller: 0.84, + arbitrage: 0.92, + hedge_reactive: 1.74, + neutral_noise: 0.76 + }, + arb_calm: { + institutional_directional: 0.68, + retail_whale: 0.58, + event_driven: 0.62, + vol_seller: 1.28, + arbitrage: 1.78, + hedge_reactive: 0.72, + neutral_noise: 1.34 + } +}; + +const REGIME_STATE_BASE: Record< + SyntheticRegime, + { + volatility: number; + liquidity: number; + quoteCleanliness: number; + offExchangeBias: number; + } +> = { + trend_up: { + volatility: 0.72, + liquidity: 0.72, + quoteCleanliness: 0.64, + offExchangeBias: 0.46 + }, + trend_down: { + volatility: 0.78, + liquidity: 0.66, + quoteCleanliness: 0.58, + offExchangeBias: 0.52 + }, + mean_revert: { + volatility: 0.5, + liquidity: 0.84, + quoteCleanliness: 0.8, + offExchangeBias: 0.34 + }, + retail_chase: { + volatility: 0.88, + liquidity: 0.62, + quoteCleanliness: 0.5, + offExchangeBias: 0.58 + }, + event_ramp: { + volatility: 0.92, + liquidity: 0.56, + quoteCleanliness: 0.42, + offExchangeBias: 0.54 + }, + dealer_gamma: { + volatility: 0.82, + liquidity: 0.66, + quoteCleanliness: 0.48, + offExchangeBias: 0.5 + }, + arb_calm: { + volatility: 0.34, + liquidity: 0.9, + quoteCleanliness: 0.88, + offExchangeBias: 0.3 + } +}; + +const clamp = (value: number, min: number, max: number): number => { + if (!Number.isFinite(value)) { + return min; + } + return Math.max(min, Math.min(max, value)); +}; + +const roundTo = (value: number, digits = 4): number => { + if (!Number.isFinite(value)) { + return 0; + } + return Number(value.toFixed(digits)); +}; + +const signedNoise = (seed: number): number => { + const raw = Math.sin(seed * 12.9898) * 43_758.5453; + return (raw - Math.floor(raw)) * 2 - 1; +}; + +const positiveNoise = (seed: number): number => { + return (signedNoise(seed) + 1) / 2; +}; + +const mixSeed = (...parts: number[]): number => { + let seed = 0x811c9dc5; + for (const part of parts) { + seed ^= Math.floor(part) >>> 0; + seed = Math.imul(seed, 0x01000193) >>> 0; + } + return seed >>> 0; +}; + +const pick = (items: readonly T[], seed: number): T => { + const index = Math.abs(seed) % items.length; + return items[index]!; +}; + +const pickManyUnique = ( + items: readonly T[], + count: number, + seed: number +): T[] => { + const pool = [...items]; + const output: T[] = []; + let cursor = seed; + while (pool.length > 0 && output.length < count) { + const index = Math.abs(cursor) % pool.length; + output.push(pool.splice(index, 1)[0]!); + cursor = mixSeed(cursor, output.length * 17 + 3); + } + return output; +}; + +const weightedPick = ( + weights: Record, + seed: number +): T => { + const entries = Object.entries(weights) as Array<[T, number]>; + const total = entries.reduce((sum, [, weight]) => sum + Math.max(0.0001, weight), 0); + let target = positiveNoise(seed) * total; + for (const [value, weight] of entries) { + target -= Math.max(0.0001, weight); + if (target <= 0) { + return value; + } + } + return entries[entries.length - 1]![0]; +}; + +const getSessionMinute = (ts: number): number => { + const minute = Math.floor(ts / 60_000); + return ((minute % 390) + 390) % 390; +}; + +export const hashSyntheticSymbol = (value: string): number => { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return hash; +}; + +export const buildEmptySyntheticProfileHitCounts = (): Record< + SmartMoneyProfileId, + number +> => ({ + institutional_directional: 0, + retail_whale: 0, + event_driven: 0, + vol_seller: 0, + arbitrage: 0, + hedge_reactive: 0 +}); + +export const normalizeSyntheticControlState = ( + control: Partial | null | undefined +): SyntheticControlState => { + const merged: SyntheticControlState = { + ...DEFAULT_SYNTHETIC_CONTROL_STATE, + ...control, + profile_weights: { + ...DEFAULT_SYNTHETIC_CONTROL_STATE.profile_weights, + ...(control?.profile_weights ?? {}) + } + }; + return SyntheticControlStateSchema.parse(merged); +}; + +const resolvePhaseBias = ( + phase: SyntheticSessionPhase, + regime: SyntheticRegime +): number => { + if (phase === "open") { + return regime === "event_ramp" ? 1.08 : 1.02; + } + if (phase === "power_hour") { + return regime === "retail_chase" || regime === "dealer_gamma" ? 1.08 : 1.03; + } + if (phase === "after_event") { + return regime === "event_ramp" ? 1.24 : 1.0; + } + return 1.0; +}; + +const resolveSessionPhase = ( + minuteOfSession: number, + eventActive: boolean, + eventOffset: number +): SyntheticSessionPhase => { + if (eventActive && eventOffset > 0.58) { + return "after_event"; + } + if (minuteOfSession < 60) { + return "open"; + } + if (minuteOfSession >= 300) { + return "power_hour"; + } + return "midday"; +}; + +export const getSyntheticSessionState = ( + ts: number, + control: Partial | null | undefined = DEFAULT_SYNTHETIC_CONTROL_STATE +): SyntheticSessionState => { + const normalized = normalizeSyntheticControlState(control); + const minuteOfSession = getSessionMinute(ts); + const bucketMs = 5 * 60_000; + const seedBucket = Math.floor(ts / bucketMs); + const presetBias = PRESET_REGIME_BIAS[normalized.preset_id]; + const eventSeed = mixSeed(normalized.shared_seed, seedBucket, normalized.updated_at); + const eventBucketOffset = positiveNoise(eventSeed + 41); + const eventActive = + normalized.preset_id === "event_day" || + eventBucketOffset > (normalized.preset_id === "balanced_demo" ? 0.72 : 0.6); + const prePhase = resolveSessionPhase(minuteOfSession, eventActive, eventBucketOffset); + const regimeWeights = REGIME_IDS.reduce( + (acc, regime) => { + const drift = 0.82 + positiveNoise(mixSeed(eventSeed, regime.length * 29)) * 0.38; + acc[regime] = presetBias[regime] * drift * resolvePhaseBias(prePhase, regime); + return acc; + }, + {} as Record + ); + const regime = weightedPick(regimeWeights, mixSeed(eventSeed, 97)); + const phase = resolveSessionPhase( + minuteOfSession, + eventActive || regime === "event_ramp", + eventBucketOffset + ); + const presetActivity = PRESET_ACTIVITY_BIAS[normalized.preset_id]; + const stateBase = REGIME_STATE_BASE[regime]; + const activitySeed = mixSeed(eventSeed, minuteOfSession, regime.length * 13); + const eventCount = + regime === "event_ramp" || phase === "after_event" + ? Math.max(2, presetActivity.eventCount) + : presetActivity.eventCount; + const focusCount = + regime === "retail_chase" || regime === "event_ramp" + ? presetActivity.focusCount + 1 + : presetActivity.focusCount; + const event_symbols: string[] = pickManyUnique( + EVENT_SYMBOL_POOL, + eventCount, + mixSeed(activitySeed, 211) + ); + const focus_symbols: string[] = pickManyUnique( + [ + ...event_symbols, + ...SYNTHETIC_SYMBOLS.filter((symbol) => !event_symbols.includes(symbol)) + ], + focusCount, + mixSeed(activitySeed, 389) + ); + const amplitude = presetActivity.amplitude; + + return { + session_phase: phase, + regime, + volatility_level: roundTo( + clamp( + stateBase.volatility * amplitude + signedNoise(activitySeed + 3) * 0.08, + 0.18, + 1.2 + ) + ), + liquidity_level: roundTo( + clamp( + stateBase.liquidity - (amplitude - 1) * 0.08 + signedNoise(activitySeed + 5) * 0.06, + 0.2, + 1.1 + ) + ), + quote_cleanliness: roundTo( + clamp( + stateBase.quoteCleanliness - (amplitude - 1) * 0.1 + signedNoise(activitySeed + 7) * 0.06, + 0.18, + 0.96 + ) + ), + focus_symbols, + event_symbols, + seed_bucket: seedBucket + }; +}; + +const isModeString = ( + value: Partial | SyntheticMarketMode | null | undefined +): value is SyntheticMarketMode => { + return value === "realistic" || value === "active" || value === "firehose"; +}; + +export const getSyntheticUnderlyingState = ( + symbol: string, + ts: number, + controlOrMode: + | Partial + | SyntheticMarketMode + | null + | undefined = DEFAULT_SYNTHETIC_CONTROL_STATE, + sessionState?: SyntheticSessionState +): SyntheticUnderlyingState => { + const control = isModeString(controlOrMode) + ? DEFAULT_SYNTHETIC_CONTROL_STATE + : normalizeSyntheticControlState(controlOrMode); + const session = sessionState ?? getSyntheticSessionState(ts, control); + const hash = hashSyntheticSymbol(symbol); + const minuteOfSession = getSessionMinute(ts); + const base = 25 + (hash % 475); + const isFocus = session.focus_symbols.includes(symbol); + const isEvent = session.event_symbols.includes(symbol); + const regimeDirection = + session.regime === "trend_up" || session.regime === "retail_chase" + ? 1 + : session.regime === "trend_down" + ? -1 + : 0; + const trendWave = + Math.sin((minuteOfSession + (hash % 71) + session.seed_bucket) / 29) * 0.55 + + Math.cos((minuteOfSession + (hash % 37) + session.seed_bucket) / 17) * 0.28; + const meanRevertWave = + Math.sin((minuteOfSession + (hash % 19)) / 6) * 0.42 - + Math.sin((minuteOfSession + (hash % 13)) / 19) * 0.24; + const eventDrift = + isEvent && (session.regime === "event_ramp" || session.session_phase === "after_event") + ? 1.25 + : 0; + const focusBoost = isFocus ? 1.18 : 0.92; + const directionBps = + regimeDirection * (14 + session.volatility_level * 36) * focusBoost + + trendWave * 22 * focusBoost + + eventDrift * 18; + const reversionBps = + session.regime === "mean_revert" || session.regime === "arb_calm" + ? -meanRevertWave * (12 + session.liquidity_level * 10) + : meanRevertWave * 6; + const gammaChop = + session.regime === "dealer_gamma" + ? Math.sin((minuteOfSession + (hash % 11)) / 2.8) * 16 + : 0; + const noiseBps = + signedNoise(mixSeed(hash, session.seed_bucket, control.shared_seed)) * + (6 + session.volatility_level * 18); + const driftBps = directionBps + reversionBps + gammaChop; + const shockBps = noiseBps + (isFocus ? signedNoise(hash + minuteOfSession) * 6 : 0); + const totalBps = driftBps + shockBps; + const mid = Math.max(0.01, Number((base * (1 + totalBps / 10_000)).toFixed(2))); + const spreadBps = + 4 + + session.volatility_level * 14 + + (1 - session.liquidity_level) * 10 + + (1 - session.quote_cleanliness) * 12 + + (session.session_phase === "open" ? 3 : 0) + + (session.session_phase === "power_hour" ? 2 : 0); + const spread = Math.max(0.01, Number((mid * (spreadBps / 10_000)).toFixed(2))); + const halfSpread = spread / 2; + const bid = Number(Math.max(0.01, mid - halfSpread).toFixed(2)); + const ask = Number(Math.max(bid + 0.01, mid + halfSpread).toFixed(2)); + const clusteringScore = clamp( + (isFocus ? 0.34 : 0.12) + + (session.regime === "dealer_gamma" ? 0.28 : 0) + + (session.regime === "retail_chase" ? 0.16 : 0), + 0, + 1 + ); + + return { + mid, + bid, + ask, + spread: Number((ask - bid).toFixed(2)), + driftBps: roundTo(driftBps), + shockBps: roundTo(shockBps), + sessionVolatility: roundTo(session.volatility_level), + liquiditySkew: roundTo(session.liquidity_level), + quoteCleanliness: roundTo(session.quote_cleanliness), + clusteringScore: roundTo(clusteringScore), + offExchangeBias: roundTo( + clamp( + REGIME_STATE_BASE[session.regime].offExchangeBias + + (isFocus ? 0.08 : 0) + + (isEvent ? 0.05 : 0), + 0.08, + 0.92 + ) + ) + }; +}; + +export const getSyntheticScenarioWeights = ( + symbol: string, + ts: number, + control: Partial | null | undefined = DEFAULT_SYNTHETIC_CONTROL_STATE, + sessionState?: SyntheticSessionState +): SyntheticScenarioWeightMap => { + const normalized = normalizeSyntheticControlState(control); + const session = sessionState ?? getSyntheticSessionState(ts, normalized); + const base = REGIME_PROFILE_BIAS[session.regime]; + const isFocus = session.focus_symbols.includes(symbol); + const isEvent = session.event_symbols.includes(symbol); + const isPower = session.session_phase === "open" || session.session_phase === "power_hour"; + const weights: SyntheticScenarioWeightMap = { + institutional_directional: base.institutional_directional, + retail_whale: base.retail_whale, + event_driven: base.event_driven, + vol_seller: base.vol_seller, + arbitrage: base.arbitrage, + hedge_reactive: base.hedge_reactive, + neutral_noise: base.neutral_noise + }; + + for (const profileId of SMART_MONEY_PROFILE_IDS) { + weights[profileId] = roundTo( + weights[profileId] * normalized.profile_weights[profileId], + 4 + ); + } + + if (isFocus) { + weights.institutional_directional = roundTo(weights.institutional_directional * 1.08, 4); + weights.retail_whale = roundTo(weights.retail_whale * 1.14, 4); + weights.hedge_reactive = roundTo(weights.hedge_reactive * 1.08, 4); + weights.neutral_noise = roundTo(weights.neutral_noise * 0.92, 4); + } + if (isEvent) { + weights.event_driven = roundTo(weights.event_driven * 1.36, 4); + weights.institutional_directional = roundTo( + weights.institutional_directional * 1.04, + 4 + ); + weights.neutral_noise = roundTo(weights.neutral_noise * 0.8, 4); + } + if (isPower) { + weights.retail_whale = roundTo(weights.retail_whale * 1.08, 4); + weights.hedge_reactive = roundTo(weights.hedge_reactive * 1.06, 4); + } + if (normalized.preset_id === "quiet_range") { + weights.neutral_noise = roundTo(weights.neutral_noise * 1.18, 4); + } + + return weights; +}; + +export const getSyntheticCoverageBoost = ( + profileId: SmartMoneyProfileId, + coverageState: SyntheticCoverageState, + control: Pick< + SyntheticControlState, + "coverage_assist" | "coverage_window_minutes" + > +): number => { + if (!control.coverage_assist) { + return 1; + } + + const counts = SMART_MONEY_PROFILE_IDS.map( + (candidate) => coverageState.profile_hit_counts[candidate] ?? 0 + ); + const targetCount = coverageState.profile_hit_counts[profileId] ?? 0; + const maxCount = Math.max(...counts); + const averageCount = + counts.reduce((sum, value) => sum + value, 0) / SMART_MONEY_PROFILE_IDS.length; + if (maxCount <= 0) { + return 1; + } + + const imbalance = clamp((maxCount - targetCount) / Math.max(1, maxCount), 0, 1); + const averageDebt = clamp(averageCount - targetCount, 0, 3); + const zeroBoost = targetCount === 0 ? 0.22 : 0; + const windowFactor = + control.coverage_window_minutes === 10 + ? 1.12 + : control.coverage_window_minutes === 30 + ? 0.94 + : 1.0; + return roundTo( + clamp(1 + (imbalance * 0.56 + averageDebt * 0.14 + zeroBoost) * windowFactor, 1, 1.86) + ); +}; + +export const getSyntheticBurstPulse = ( + ts: number, + controlOrMode: + | Partial + | SyntheticMarketMode + | null + | undefined = DEFAULT_SYNTHETIC_CONTROL_STATE +): SyntheticBurstPulse => { + const control = isModeString(controlOrMode) + ? DEFAULT_SYNTHETIC_CONTROL_STATE + : normalizeSyntheticControlState(controlOrMode); + const session = getSyntheticSessionState(ts, control); + return { + active: session.regime !== "arb_calm" || session.focus_symbols.length > 1, + intensity: roundTo( + clamp( + session.volatility_level * 0.72 + + session.focus_symbols.length * 0.06 - + session.quote_cleanliness * 0.08, + 0.12, + 1 + ) + ), + focusSymbols: [...session.focus_symbols], + bucket: session.seed_bucket + }; +}; + +export const SYNTHETIC_CONTROL_METADATA = { + profileWeightValues: SYNTHETIC_PROFILE_WEIGHT_VALUES, + coverageWindowValues: SYNTHETIC_COVERAGE_WINDOW_VALUES, + smartMoneyProfileIds: SMART_MONEY_PROFILE_IDS +} as const; diff --git a/packages/types/tests/synthetic-market.test.ts b/packages/types/tests/synthetic-market.test.ts new file mode 100644 index 0000000..03e5117 --- /dev/null +++ b/packages/types/tests/synthetic-market.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "bun:test"; +import { + DEFAULT_SYNTHETIC_CONTROL_STATE, + buildEmptySyntheticProfileHitCounts, + getSyntheticCoverageBoost, + getSyntheticScenarioWeights, + getSyntheticSessionState, + getSyntheticUnderlyingState +} from "../src/synthetic-market"; + +describe("synthetic market regime engine", () => { + it("is deterministic for the same timestamp, control, and seed", () => { + const ts = Date.parse("2026-01-14T15:25:00Z"); + const sessionA = getSyntheticSessionState(ts, DEFAULT_SYNTHETIC_CONTROL_STATE); + const sessionB = getSyntheticSessionState(ts, DEFAULT_SYNTHETIC_CONTROL_STATE); + const underlyingA = getSyntheticUnderlyingState( + "NVDA", + ts, + DEFAULT_SYNTHETIC_CONTROL_STATE, + sessionA + ); + const underlyingB = getSyntheticUnderlyingState( + "NVDA", + ts, + DEFAULT_SYNTHETIC_CONTROL_STATE, + sessionB + ); + + expect(sessionA).toEqual(sessionB); + expect(underlyingA).toEqual(underlyingB); + }); + + it("makes quiet range calmer than retail chase", () => { + const ts = Date.parse("2026-01-14T17:10:00Z"); + const quietControl = { + ...DEFAULT_SYNTHETIC_CONTROL_STATE, + preset_id: "quiet_range" as const + }; + const chaseControl = { + ...DEFAULT_SYNTHETIC_CONTROL_STATE, + preset_id: "retail_chase" as const + }; + const quietSession = getSyntheticSessionState(ts, quietControl); + const chaseSession = getSyntheticSessionState(ts, chaseControl); + const quietState = getSyntheticUnderlyingState("AAPL", ts, quietControl, quietSession); + const chaseState = getSyntheticUnderlyingState("AAPL", ts, chaseControl, chaseSession); + + expect(quietSession.volatility_level).toBeLessThan(chaseSession.volatility_level); + expect(quietState.spread).toBeLessThanOrEqual(chaseState.spread); + expect(quietState.sessionVolatility).toBeLessThan(chaseState.sessionVolatility); + }); + + it("materially tilts family weights by preset and regime", () => { + const ts = Date.parse("2026-01-14T19:40:00Z"); + const eventControl = { + ...DEFAULT_SYNTHETIC_CONTROL_STATE, + preset_id: "event_day" as const + }; + const quietControl = { + ...DEFAULT_SYNTHETIC_CONTROL_STATE, + preset_id: "quiet_range" as const + }; + const eventSession = getSyntheticSessionState(ts, eventControl); + const quietSession = getSyntheticSessionState(ts, quietControl); + const eventWeights = getSyntheticScenarioWeights("AAPL", ts, eventControl, eventSession); + const quietWeights = getSyntheticScenarioWeights("AAPL", ts, quietControl, quietSession); + + expect(eventWeights.event_driven).toBeGreaterThan(quietWeights.event_driven); + expect(quietWeights.neutral_noise).toBeGreaterThan(eventWeights.neutral_noise); + }); +}); + +describe("synthetic coverage assist", () => { + it("boosts under-hit profiles without forcing when enabled", () => { + const counts = buildEmptySyntheticProfileHitCounts(); + counts.institutional_directional = 3; + counts.arbitrage = 2; + + const boost = getSyntheticCoverageBoost( + "event_driven", + { profile_hit_counts: counts }, + DEFAULT_SYNTHETIC_CONTROL_STATE + ); + + expect(boost).toBeGreaterThan(1); + expect(boost).toBeLessThanOrEqual(1.86); + }); + + it("returns neutral boost when coverage assist is disabled", () => { + const counts = buildEmptySyntheticProfileHitCounts(); + counts.institutional_directional = 4; + + expect( + getSyntheticCoverageBoost( + "event_driven", + { profile_hit_counts: counts }, + { + coverage_assist: false, + coverage_window_minutes: 20 + } + ) + ).toBe(1); + }); +}); diff --git a/services/api/src/index.ts b/services/api/src/index.ts index a857e02..39fba48 100644 --- a/services/api/src/index.ts +++ b/services/api/src/index.ts @@ -25,8 +25,12 @@ import { STREAM_OPTION_SIGNAL_PRINTS, buildDurableConsumer, connectJetStreamWithRetry, + ensureSyntheticControlState, ensureKnownStreams, - subscribeJson + openSyntheticControlKv, + subscribeJson, + watchSyntheticControlState, + writeSyntheticControlState } from "@islandflow/bus"; import { createClickHouseClient, @@ -100,6 +104,7 @@ import { matchesFlowPacketFilters, matchesOptionPrintFilters, FlowPacketSchema, + SyntheticControlStateSchema, SmartMoneyEventSchema, OptionNBBOSchema, OptionPrintSchema, @@ -114,6 +119,13 @@ import { shouldFanoutLiveEvent } from "./live"; import { parseOptionPrintQuery } from "./option-queries"; +import { + buildSyntheticDerivedStatus, + createRollingSyntheticProfileHits, + getSyntheticBackendDisabledReason, + recordSyntheticProfileHit, + resolveSyntheticBackendMode +} from "./synthetic-control"; const service = "api"; const logger = createLogger({ service }); @@ -127,10 +139,27 @@ const envSchema = z.object({ CLICKHOUSE_URL: z.string().default("http://127.0.0.1:8123"), CLICKHOUSE_DATABASE: z.string().default("default"), REDIS_URL: z.string().default("redis://127.0.0.1:6379"), + OPTIONS_INGEST_ADAPTER: z.string().min(1).default("synthetic"), + EQUITIES_INGEST_ADAPTER: z.string().min(1).default("synthetic"), REST_DEFAULT_LIMIT: z.coerce.number().int().positive().default(200), API_DELIVER_POLICY: DeliverPolicySchema.default("new"), API_CONSUMER_RESET: z.coerce.boolean().default(false), - LIVE_LAG_WARN_MS: z.coerce.number().int().positive().default(120_000) + LIVE_LAG_WARN_MS: z.coerce.number().int().positive().default(120_000), + SYNTHETIC_CONTROL_ENABLED: z + .preprocess((value) => { + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (["1", "true", "yes", "on"].includes(normalized)) { + return true; + } + if (["0", "false", "no", "off"].includes(normalized)) { + return false; + } + } + return value; + }, z.boolean()) + .default(false), + SYNTHETIC_ADMIN_TOKEN: z.string().default("") }); const env = readEnv(envSchema); @@ -283,6 +312,14 @@ const readJsonBody = async (req: Request): Promise => { return JSON.parse(text); }; +const getBearerToken = (req: Request): string => { + const authorization = req.headers.get("authorization") ?? ""; + if (authorization.toLowerCase().startsWith("bearer ")) { + return authorization.slice(7).trim(); + } + return req.headers.get("x-synthetic-admin-token")?.trim() ?? ""; +}; + const optionsSupportLookupSchema = z.object({ trace_ids: z.array(z.string().min(1)).default([]), nbbo_context: z @@ -641,6 +678,27 @@ const run = async () => { { logger } ); + const syntheticBackendMode = resolveSyntheticBackendMode( + env.OPTIONS_INGEST_ADAPTER, + env.EQUITIES_INGEST_ADAPTER + ); + const syntheticBackendDisabledReason = + getSyntheticBackendDisabledReason(syntheticBackendMode); + const syntheticControlKv = await openSyntheticControlKv(js); + let syntheticControl = await ensureSyntheticControlState(syntheticControlKv); + const syntheticProfileHits = createRollingSyntheticProfileHits(); + const stopSyntheticControlWatch = await watchSyntheticControlState( + syntheticControlKv, + (nextControl) => { + syntheticControl = nextControl; + }, + (error) => { + logger.warn("synthetic control watch failed", { + error: getErrorMessage(error) + }); + } + ); + const clickhouse = createClickHouseClient({ url: env.CLICKHOUSE_URL, database: env.CLICKHOUSE_DATABASE @@ -1146,6 +1204,7 @@ const run = async () => { for await (const msg of smartMoneySubscription.messages) { try { const payload = SmartMoneyEventSchema.parse(smartMoneySubscription.decode(msg)); + recordSyntheticProfileHit(syntheticProfileHits, payload); broadcast(smartMoneySockets, { type: "smart-money", payload }); await fanoutLive({ channel: "smart-money" }, payload, "smart-money"); msg.ack(); @@ -1202,6 +1261,54 @@ const run = async () => { void pumpClassifierHits(); void pumpAlerts(); + const buildSyntheticStatusBody = () => { + const derived = + syntheticBackendMode === "synthetic" + ? buildSyntheticDerivedStatus(Date.now(), syntheticControl, syntheticProfileHits) + : null; + return { + enabled: env.SYNTHETIC_CONTROL_ENABLED && syntheticBackendMode === "synthetic", + backend_mode: syntheticBackendMode, + adapters: { + options: env.OPTIONS_INGEST_ADAPTER, + equities: env.EQUITIES_INGEST_ADAPTER + }, + control: syntheticBackendMode === "synthetic" ? syntheticControl : null, + derived, + ...(syntheticBackendDisabledReason + ? { disabled_reason: syntheticBackendDisabledReason } + : {}) + }; + }; + + const authenticateSyntheticAdminRequest = (req: Request): Response | null => { + if (!env.SYNTHETIC_CONTROL_ENABLED) { + return jsonResponse({ error: "not found" }, 404); + } + if (!env.SYNTHETIC_ADMIN_TOKEN) { + return jsonResponse( + { + error: "synthetic admin misconfigured", + detail: "SYNTHETIC_ADMIN_TOKEN is required when synthetic control is enabled." + }, + 500 + ); + } + if (getBearerToken(req) !== env.SYNTHETIC_ADMIN_TOKEN) { + return jsonResponse({ error: "unauthorized" }, 401); + } + if (syntheticBackendMode !== "synthetic") { + return jsonResponse( + { + error: "synthetic backend unavailable", + ...buildSyntheticStatusBody() + }, + 409 + ); + } + return null; + }; + const server = Bun.serve({ port: env.API_PORT, fetch: async (req: Request, serverRef: any) => { @@ -1211,6 +1318,49 @@ const run = async () => { return jsonResponse({ status: "ok" }); } + if (req.method === "GET" && url.pathname === "/admin/synthetic/status") { + const authError = authenticateSyntheticAdminRequest(req); + if (authError) { + return authError; + } + return jsonResponse(buildSyntheticStatusBody()); + } + + if (req.method === "GET" && url.pathname === "/admin/synthetic/control") { + const authError = authenticateSyntheticAdminRequest(req); + if (authError) { + return authError; + } + return jsonResponse({ control: syntheticControl }); + } + + if (req.method === "PUT" && url.pathname === "/admin/synthetic/control") { + const authError = authenticateSyntheticAdminRequest(req); + if (authError) { + return authError; + } + try { + const payload = SyntheticControlStateSchema.parse(await readJsonBody(req)); + syntheticControl = await writeSyntheticControlState(syntheticControlKv, payload); + return jsonResponse({ + control: syntheticControl, + derived: buildSyntheticDerivedStatus( + Date.now(), + syntheticControl, + syntheticProfileHits + ) + }); + } catch (error) { + return jsonResponse( + { + error: "invalid synthetic control payload", + detail: getErrorMessage(error) + }, + 400 + ); + } + } + if (req.method === "GET" && url.pathname === "/prints/options") { try { const limit = parseLimit(url.searchParams.get("limit")); @@ -1824,6 +1974,7 @@ const run = async () => { logger.info("service stopping", { signal }); server.stop(); clearInterval(liveStateMetricsTimer); + await stopSyntheticControlWatch(); await liveState.close(); if (redis && redis.isOpen) { diff --git a/services/api/src/synthetic-control.ts b/services/api/src/synthetic-control.ts new file mode 100644 index 0000000..cbc310b --- /dev/null +++ b/services/api/src/synthetic-control.ts @@ -0,0 +1,93 @@ +import { + SyntheticDerivedStatusSchema, + buildEmptySyntheticProfileHitCounts, + getSyntheticSessionState, + type SmartMoneyEvent, + type SmartMoneyProfileId, + type SyntheticControlState, + type SyntheticDerivedStatus +} from "@islandflow/types"; + +export type SyntheticBackendMode = "synthetic" | "mixed" | "live"; + +export type RollingSyntheticProfileHits = Record; + +export const createRollingSyntheticProfileHits = (): RollingSyntheticProfileHits => ({ + institutional_directional: [], + retail_whale: [], + event_driven: [], + vol_seller: [], + arbitrage: [], + hedge_reactive: [] +}); + +export const resolveSyntheticBackendMode = ( + optionsAdapter: string, + equitiesAdapter: string +): SyntheticBackendMode => { + const optionsSynthetic = optionsAdapter === "synthetic"; + const equitiesSynthetic = equitiesAdapter === "synthetic"; + if (optionsSynthetic && equitiesSynthetic) { + return "synthetic"; + } + if (optionsSynthetic || equitiesSynthetic) { + return "mixed"; + } + return "live"; +}; + +export const getSyntheticBackendDisabledReason = ( + mode: SyntheticBackendMode +): string | undefined => { + if (mode === "synthetic") { + return undefined; + } + if (mode === "mixed") { + return "Synthetic control requires both hosted ingest adapters to run in synthetic mode."; + } + return "Hosted ingest adapters are not synthetic, so the internal synthetic control surface is unavailable."; +}; + +export const recordSyntheticProfileHit = ( + state: RollingSyntheticProfileHits, + event: Pick +): void => { + if (!event.primary_profile_id) { + return; + } + state[event.primary_profile_id].push(event.source_ts); +}; + +export const getSyntheticProfileHitCounts = ( + state: RollingSyntheticProfileHits, + now: number, + coverageWindowMinutes: number +): Record => { + const floorTs = now - coverageWindowMinutes * 60_000; + const counts = buildEmptySyntheticProfileHitCounts(); + for (const profileId of Object.keys(state) as SmartMoneyProfileId[]) { + const retained = state[profileId].filter((ts) => ts >= floorTs); + state[profileId] = retained; + counts[profileId] = retained.length; + } + return counts; +}; + +export const buildSyntheticDerivedStatus = ( + now: number, + control: SyntheticControlState, + state: RollingSyntheticProfileHits +): SyntheticDerivedStatus => { + const session = getSyntheticSessionState(now, control); + return SyntheticDerivedStatusSchema.parse({ + session_phase: session.session_phase, + regime: session.regime, + focus_symbols: session.focus_symbols, + profile_hit_counts: getSyntheticProfileHitCounts( + state, + now, + control.coverage_window_minutes + ), + coverage_window_minutes: control.coverage_window_minutes + }); +}; diff --git a/services/api/tests/synthetic-control.test.ts b/services/api/tests/synthetic-control.test.ts new file mode 100644 index 0000000..b7090f5 --- /dev/null +++ b/services/api/tests/synthetic-control.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "bun:test"; +import { DEFAULT_SYNTHETIC_CONTROL_STATE } from "@islandflow/types"; +import { + buildSyntheticDerivedStatus, + createRollingSyntheticProfileHits, + getSyntheticBackendDisabledReason, + getSyntheticProfileHitCounts, + recordSyntheticProfileHit, + resolveSyntheticBackendMode +} from "../src/synthetic-control"; + +describe("synthetic control backend mode", () => { + it("detects synthetic, mixed, and live hosted modes", () => { + expect(resolveSyntheticBackendMode("synthetic", "synthetic")).toBe("synthetic"); + expect(resolveSyntheticBackendMode("synthetic", "alpaca")).toBe("mixed"); + expect(resolveSyntheticBackendMode("alpaca", "alpaca")).toBe("live"); + }); + + it("provides a useful disabled reason for non-synthetic modes", () => { + expect(getSyntheticBackendDisabledReason("mixed")).toContain("both hosted ingest adapters"); + expect(getSyntheticBackendDisabledReason("live")).toContain("not synthetic"); + }); +}); + +describe("synthetic control rolling status", () => { + it("tracks public-profile hits inside the rolling coverage window", () => { + const hits = createRollingSyntheticProfileHits(); + + recordSyntheticProfileHit(hits, { + primary_profile_id: "event_driven", + source_ts: 1_000 + }); + recordSyntheticProfileHit(hits, { + primary_profile_id: "event_driven", + source_ts: 60_000 + }); + recordSyntheticProfileHit(hits, { + primary_profile_id: "arbitrage", + source_ts: 70_000 + }); + + expect(getSyntheticProfileHitCounts(hits, 11 * 60_000, 10)).toEqual({ + institutional_directional: 0, + retail_whale: 0, + event_driven: 1, + vol_seller: 0, + arbitrage: 1, + hedge_reactive: 0 + }); + }); + + it("builds derived status from the shared session engine", () => { + const hits = createRollingSyntheticProfileHits(); + recordSyntheticProfileHit(hits, { + primary_profile_id: "hedge_reactive", + source_ts: Date.parse("2026-01-14T18:00:00Z") + }); + + const derived = buildSyntheticDerivedStatus( + Date.parse("2026-01-14T18:05:00Z"), + DEFAULT_SYNTHETIC_CONTROL_STATE, + hits + ); + + expect(derived.coverage_window_minutes).toBe(20); + expect(derived.focus_symbols.length).toBeGreaterThan(0); + expect(derived.profile_hit_counts.hedge_reactive).toBe(1); + }); +}); diff --git a/services/ingest-equities/src/adapters/synthetic.ts b/services/ingest-equities/src/adapters/synthetic.ts index 01a2de3..59e0a98 100644 --- a/services/ingest-equities/src/adapters/synthetic.ts +++ b/services/ingest-equities/src/adapters/synthetic.ts @@ -1,7 +1,10 @@ import { SP500_SYMBOLS, + getSyntheticSessionState, + getSyntheticUnderlyingState, type EquityPrint, type EquityQuote, + type SyntheticControlState, type SyntheticMarketMode } from "@islandflow/types"; import type { EquityIngestAdapter, EquityIngestHandlers } from "./types"; @@ -9,34 +12,14 @@ import type { EquityIngestAdapter, EquityIngestHandlers } from "./types"; type SyntheticEquitiesAdapterConfig = { emitIntervalMs: number; mode: SyntheticMarketMode; + getControl: () => SyntheticControlState; }; -const EXCHANGES = ["NYSE", "NASDAQ", "ARCA", "BATS", "IEX", "TEST"]; +const EXCHANGES = ["NYSE", "NASDAQ", "ARCA", "BATS", "IEX", "MEMX"]; const DARK_EXCHANGE = "OTC"; - -type PricePlacement = "MID" | "A" | "AA" | "B" | "BB"; -type DarkScenario = "block" | "buy" | "sell"; - -const DARK_SEQUENCE: DarkScenario[] = [ - "block", - "buy", - "buy", - "buy", - "buy", - "sell", - "sell", - "sell", - "sell" -]; const SYNTHETIC_SYMBOLS = ["SPY", ...(SP500_SYMBOLS as readonly string[])]; -const hashSymbol = (value: string): number => { - let hash = 0; - for (let i = 0; i < value.length; i += 1) { - hash = (hash * 31 + value.charCodeAt(i)) >>> 0; - } - return hash; -}; +type PricePlacement = "MID" | "A" | "AA" | "B" | "BB"; const buildSyntheticPrint = ( seq: number, @@ -46,20 +29,18 @@ const buildSyntheticPrint = ( size: number, exchange: string, offExchangeFlag: boolean -): EquityPrint => { - return { - source_ts: now, - ingest_ts: now, - seq, - trace_id: `synthetic-equities-${seq}`, - ts: now, - underlying_id: symbol, - price, - size, - exchange, - offExchangeFlag - }; -}; +): EquityPrint => ({ + source_ts: now, + ingest_ts: now, + seq, + trace_id: `synthetic-equities-${seq}`, + ts: now, + underlying_id: symbol, + price, + size, + exchange, + offExchangeFlag +}); const buildSyntheticQuote = ( seq: number, @@ -67,32 +48,18 @@ const buildSyntheticQuote = ( symbol: string, bid: number, ask: number -): EquityQuote => { - return { - source_ts: now, - ingest_ts: now, - seq, - trace_id: `synthetic-equity-quote-${seq}`, - ts: now, - underlying_id: symbol, - bid, - ask - }; -}; +): EquityQuote => ({ + source_ts: now, + ingest_ts: now, + seq, + trace_id: `synthetic-equity-quote-${seq}`, + ts: now, + underlying_id: symbol, + bid, + ask +}); -const formatPrice = (value: number): number => { - return Number(value.toFixed(2)); -}; - -const buildQuoteFromMid = (mid: number) => { - const spread = Math.max(0.05, Number((mid * 0.002).toFixed(2))); - const half = spread / 2; - const bid = formatPrice(Math.max(0.01, mid - half)); - const ask = formatPrice(Math.max(bid + 0.01, mid + half)); - const epsilon = Math.max(0.01, spread * 0.05); - - return { bid, ask, spread, epsilon }; -}; +const formatPrice = (value: number): number => Number(value.toFixed(2)); const priceForPlacement = ( mid: number, @@ -100,7 +67,6 @@ const priceForPlacement = ( placement: PricePlacement ): number => { const { bid, ask, epsilon } = quote; - let price = mid; switch (placement) { case "AA": @@ -120,44 +86,83 @@ const priceForPlacement = ( price = mid; break; } - return formatPrice(Math.max(0.01, price)); }; +const buildQuoteContext = ( + symbol: string, + now: number, + control: SyntheticControlState +) => { + const session = getSyntheticSessionState(now, control); + const state = getSyntheticUnderlyingState(symbol, now, control, session); + return { + session, + state, + mid: state.mid, + bid: formatPrice(state.bid), + ask: formatPrice(state.ask), + spread: state.spread, + epsilon: Math.max(0.01, state.spread * 0.08) + }; +}; + +const pickPrimaryPlacement = ( + driftBps: number, + regime: ReturnType["regime"], + seq: number +): PricePlacement => { + if (regime === "dealer_gamma") { + return seq % 4 === 0 ? "A" : seq % 3 === 0 ? "B" : "MID"; + } + if (regime === "arb_calm" || regime === "mean_revert") { + return seq % 11 === 0 ? "A" : seq % 13 === 0 ? "B" : "MID"; + } + if (regime === "event_ramp" || regime === "retail_chase") { + if (driftBps >= 0) { + return seq % 3 === 0 ? "AA" : "A"; + } + return seq % 3 === 0 ? "BB" : "B"; + } + if (driftBps >= 0) { + return seq % 5 === 0 ? "A" : "MID"; + } + return seq % 5 === 0 ? "B" : "MID"; +}; + +const pickDarkPlacement = ( + driftBps: number, + regime: ReturnType["regime"], + seq: number +): PricePlacement => { + if (regime === "dealer_gamma") { + return seq % 2 === 0 ? "A" : "B"; + } + if (regime === "arb_calm" || regime === "mean_revert") { + return "MID"; + } + if (regime === "event_ramp" || regime === "retail_chase") { + return driftBps >= 0 ? (seq % 2 === 0 ? "A" : "AA") : seq % 2 === 0 ? "B" : "BB"; + } + return driftBps >= 0 ? "A" : "B"; +}; + export const createSyntheticEquitiesAdapter = ( config: SyntheticEquitiesAdapterConfig ): EquityIngestAdapter => { - const profile = + const throughput = config.mode === "firehose" - ? { - batchSize: 10, - darkEvery: true, - offExchangeMod: 2, - litSizeBase: 40, - litSizeRange: 1400 - } + ? { batchSize: 10, litSizeBase: 48, litSizeRange: 1800, darkSizeBase: 2800 } : config.mode === "active" - ? { - batchSize: 5, - darkEvery: true, - offExchangeMod: 4, - litSizeBase: 20, - litSizeRange: 900 - } - : { - batchSize: 2, - darkEvery: false, - offExchangeMod: 8, - litSizeBase: 10, - litSizeRange: 300 - }; + ? { batchSize: 5, litSizeBase: 22, litSizeRange: 980, darkSizeBase: 1800 } + : { batchSize: 2, litSizeBase: 12, litSizeRange: 340, darkSizeBase: 900 }; + return { name: "synthetic", start: (handlers: EquityIngestHandlers) => { let seq = 0; let quoteSeq = 0; - let darkStep = 0; - let darkSymbolIndex = 0; + let symbolCursor = 0; let timer: ReturnType | null = null; let stopped = false; @@ -167,84 +172,113 @@ export const createSyntheticEquitiesAdapter = ( } const now = Date.now(); - const batchSize = profile.batchSize; + const control = config.getControl(); + const session = getSyntheticSessionState(now, control); + const focusSymbols = + session.focus_symbols.length > 0 ? session.focus_symbols : SYNTHETIC_SYMBOLS.slice(0, 3); + const focusSet = new Set(focusSymbols); + const allowDark = + config.mode !== "realistic" || + session.regime === "event_ramp" || + session.regime === "dealer_gamma" || + session.regime === "retail_chase"; - const darkSymbol = SYNTHETIC_SYMBOLS[darkSymbolIndex % SYNTHETIC_SYMBOLS.length]; - const darkHash = hashSymbol(darkSymbol); - const darkBase = 25 + (darkHash % 475); - const darkDrift = ((darkStep % 24) - 12) * 0.08; - const darkMid = formatPrice(darkBase + darkDrift); - const darkQuote = buildQuoteFromMid(darkMid); - const scenario = DARK_SEQUENCE[darkStep % DARK_SEQUENCE.length]; - const darkTs = now; - - if (profile.darkEvery) { - if (handlers.onQuote) { - quoteSeq += 1; - const quoteEvent = buildSyntheticQuote( - quoteSeq, - darkTs - 2, - darkSymbol, - darkQuote.bid, - darkQuote.ask - ); - void handlers.onQuote(quoteEvent); - } - - seq += 1; - let darkPlacement: PricePlacement = "MID"; - let darkSize = config.mode === "firehose" ? 4000 : 2600; - if (scenario === "buy") { - darkPlacement = darkStep % 2 === 0 ? "A" : "AA"; - darkSize = config.mode === "firehose" ? 1500 : 800; - } else if (scenario === "sell") { - darkPlacement = darkStep % 2 === 0 ? "B" : "BB"; - darkSize = config.mode === "firehose" ? 1500 : 800; - } - const darkPrice = priceForPlacement(darkMid, darkQuote, darkPlacement); - const darkPrint = buildSyntheticPrint( - seq, - darkTs, - darkSymbol, - darkPrice, - darkSize, - DARK_EXCHANGE, - true + if (allowDark) { + const darkSymbol = focusSymbols[seq % focusSymbols.length] ?? SYNTHETIC_SYMBOLS[symbolCursor % SYNTHETIC_SYMBOLS.length]!; + const darkQuote = buildQuoteContext(darkSymbol, now, control); + const darkPlacement = pickDarkPlacement( + darkQuote.state.driftBps, + session.regime, + seq + 1 + ); + const darkBias = darkQuote.state.offExchangeBias; + const darkSize = Math.max( + 250, + Math.round( + throughput.darkSizeBase * + (0.65 + darkBias * 0.9 + darkQuote.state.sessionVolatility * 0.2) + ) ); - void handlers.onTrade(darkPrint); - - darkStep += 1; - if (darkStep >= DARK_SEQUENCE.length) { - darkStep = 0; - darkSymbolIndex += 1; - } - } - - for (let i = 0; i < batchSize; i += 1) { - seq += 1; - const symbol = SYNTHETIC_SYMBOLS[(seq + i) % SYNTHETIC_SYMBOLS.length]; - const symbolHash = hashSymbol(symbol); - const basePrice = 25 + (symbolHash % 475); - const mid = formatPrice(basePrice + ((seq % 40) - 20) * 0.05); - const quote = buildQuoteFromMid(mid); - const placement: PricePlacement = - seq % 11 === 0 ? "A" : seq % 13 === 0 ? "B" : "MID"; - const price = priceForPlacement(mid, quote, placement); - const size = profile.litSizeBase + (seq % profile.litSizeRange); - const exchange = EXCHANGES[(seq + symbolHash) % EXCHANGES.length]; - const offExchangeFlag = (seq + i) % profile.offExchangeMod === 0; - const eventTs = now + i * 4; if (handlers.onQuote) { quoteSeq += 1; - const quoteEventTs = eventTs - 2; - const quoteEvent = buildSyntheticQuote(quoteSeq, quoteEventTs, symbol, quote.bid, quote.ask); - void handlers.onQuote(quoteEvent); + void handlers.onQuote( + buildSyntheticQuote( + quoteSeq, + now - 2, + darkSymbol, + darkQuote.bid, + darkQuote.ask + ) + ); } - const print = buildSyntheticPrint(seq, eventTs, symbol, price, size, exchange, offExchangeFlag); - void handlers.onTrade(print); + seq += 1; + void handlers.onTrade( + buildSyntheticPrint( + seq, + now, + darkSymbol, + priceForPlacement(darkQuote.mid, darkQuote, darkPlacement), + darkSize, + DARK_EXCHANGE, + true + ) + ); } + + for (let i = 0; i < throughput.batchSize; i += 1) { + seq += 1; + const symbol = + i < focusSymbols.length + ? focusSymbols[i]! + : SYNTHETIC_SYMBOLS[(symbolCursor + i) % SYNTHETIC_SYMBOLS.length]!; + const eventTs = now + i * 4; + const quote = buildQuoteContext(symbol, eventTs, control); + const clustered = focusSet.has(symbol); + const placement = pickPrimaryPlacement( + quote.state.driftBps, + session.regime, + seq + i + ); + const exchange = EXCHANGES[(seq + symbol.charCodeAt(0) + i) % EXCHANGES.length]!; + const baseSize = + throughput.litSizeBase + + ((seq + i) % throughput.litSizeRange) + + Math.round(quote.state.sessionVolatility * 140); + const size = clustered + ? Math.round(baseSize * (1 + quote.state.clusteringScore * 0.35)) + : baseSize; + const offExchangeFlag = + ((seq + i * 3) % 10) / 10 < quote.state.offExchangeBias * (clustered ? 1.12 : 0.86); + + if (handlers.onQuote) { + quoteSeq += 1; + void handlers.onQuote( + buildSyntheticQuote( + quoteSeq, + eventTs - 2, + symbol, + quote.bid, + quote.ask + ) + ); + } + + void handlers.onTrade( + buildSyntheticPrint( + seq, + eventTs, + symbol, + priceForPlacement(quote.mid, quote, placement), + size, + exchange, + offExchangeFlag + ) + ); + } + + symbolCursor = (symbolCursor + throughput.batchSize) % SYNTHETIC_SYMBOLS.length; }; timer = setInterval(emit, config.emitIntervalMs); diff --git a/services/ingest-equities/src/index.ts b/services/ingest-equities/src/index.ts index e65231e..f098b15 100644 --- a/services/ingest-equities/src/index.ts +++ b/services/ingest-equities/src/index.ts @@ -6,7 +6,10 @@ import { STREAM_EQUITY_PRINTS, STREAM_EQUITY_QUOTES, connectJetStreamWithRetry, + ensureSyntheticControlState, ensureKnownStreams, + openSyntheticControlKv, + watchSyntheticControlState, publishJson } from "@islandflow/bus"; import { @@ -19,9 +22,11 @@ import { import { EquityPrintSchema, EquityQuoteSchema, + DEFAULT_SYNTHETIC_CONTROL_STATE, resolveSyntheticMarketModes, type EquityPrint, - type EquityQuote + type EquityQuote, + type SyntheticControlState } from "@islandflow/types"; import { createAlpacaEquitiesAdapter } from "./adapters/alpaca"; import { createSyntheticEquitiesAdapter } from "./adapters/synthetic"; @@ -157,11 +162,15 @@ const parseSymbolList = (value: string): string[] => { .filter(Boolean); }; -const selectAdapter = (name: string): EquityIngestAdapter => { +const selectAdapter = ( + name: string, + getSyntheticControl: () => SyntheticControlState +): EquityIngestAdapter => { if (name === "synthetic") { return createSyntheticEquitiesAdapter({ emitIntervalMs: env.EMIT_INTERVAL_MS, - mode: syntheticModes.equities + mode: syntheticModes.equities, + getControl: getSyntheticControl }); } @@ -196,6 +205,24 @@ const run = async () => { await ensureKnownStreams(jsm, [STREAM_EQUITY_PRINTS, STREAM_EQUITY_QUOTES], { logger }); + let syntheticControl = DEFAULT_SYNTHETIC_CONTROL_STATE; + let stopSyntheticControlWatch = async () => {}; + if (env.EQUITIES_INGEST_ADAPTER === "synthetic") { + const syntheticControlKv = await openSyntheticControlKv(js); + syntheticControl = await ensureSyntheticControlState(syntheticControlKv); + stopSyntheticControlWatch = await watchSyntheticControlState( + syntheticControlKv, + (nextControl) => { + syntheticControl = nextControl; + }, + (error) => { + logger.warn("synthetic control watch failed", { + error: getErrorMessage(error) + }); + } + ); + } + const clickhouse = createClickHouseClient({ url: env.CLICKHOUSE_URL, database: env.CLICKHOUSE_DATABASE @@ -206,7 +233,10 @@ const run = async () => { await ensureEquityQuotesTable(clickhouse); }); - const adapter = selectAdapter(env.EQUITIES_INGEST_ADAPTER); + const adapter = selectAdapter( + env.EQUITIES_INGEST_ADAPTER, + () => syntheticControl + ); logger.info("ingest adapter selected", { adapter: adapter.name }); const allowPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS); const allowQuotePublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS); @@ -274,6 +304,7 @@ const run = async () => { state.shuttingDown = true; state.shutdownPromise = (async () => { logger.info("service stopping", { signal }); + await stopSyntheticControlWatch(); await stopAdapter(); try { diff --git a/services/ingest-options/src/adapters/synthetic.ts b/services/ingest-options/src/adapters/synthetic.ts index eaa3f02..226f87c 100644 --- a/services/ingest-options/src/adapters/synthetic.ts +++ b/services/ingest-options/src/adapters/synthetic.ts @@ -1,9 +1,16 @@ import { SP500_SYMBOLS, + buildEmptySyntheticProfileHitCounts, + getSyntheticCoverageBoost, + getSyntheticScenarioWeights, + getSyntheticSessionState, + getSyntheticUnderlyingState, + hashSyntheticSymbol, type FlowPacket, type OptionNBBO, type OptionPrint, type SmartMoneyProfileId, + type SyntheticControlState, type SyntheticMarketMode } from "@islandflow/types"; import type { OptionIngestAdapter, OptionIngestHandlers } from "./types"; @@ -11,6 +18,18 @@ import type { OptionIngestAdapter, OptionIngestHandlers } from "./types"; type SyntheticOptionsAdapterConfig = { emitIntervalMs: number; mode: SyntheticMarketMode; + getControl?: () => SyntheticControlState; +}; + +type BurstLeg = { + contractId: string; + right: "C" | "P"; + expiryOffsetDays: number; + strike: number; + basePrice: number; + baseSize: number; + exchange: string; + placementScenarioId: string; }; type Burst = { @@ -20,14 +39,62 @@ type Burst = { strike: number; basePrice: number; baseSize: number; - exchange: string; - conditions?: string[]; + legs: BurstLeg[]; + conditions: string[]; + cycles: number; printCount: number; priceStep: number; scenarioId: string; label: SyntheticScenarioLabel; + hiddenLabel: string; seed: number; flowFeatures: FlowPacket["features"]; + missingQuoteProbability: number; + staleQuoteProbability: number; +}; + +type ScenarioLegTemplate = { + right: "C" | "P"; + strikeMoneyness?: number; + strikeOffsetSteps?: number; + expiryOffsetDays?: number; + priceMultiplier?: number; + sizeMultiplier?: number; + placementScenarioId?: string; +}; + +type Scenario = { + id: string; + hiddenLabel: string; + label: SyntheticScenarioLabel; + right: "C" | "P" | "either"; + weight: number; + countRange: [number, number]; + sizeRange: [number, number]; + targetNotionalRange: [number, number]; + priceTrend: "up" | "down" | "flat"; + expiryOffsets?: number[]; + strikeMoneyness?: number; + preferredSymbols?: string[]; + placementProfile?: SyntheticScenarioLabel; + missingQuoteProbability?: number; + staleQuoteProbability?: number; + conditions?: string[]; + flowFeatures: FlowPacket["features"]; + legs?: ScenarioLegTemplate[]; +}; + +type WeightedValue = { + value: T; + weight: number; +}; + +type CoverageWindowState = Record; + +type SyntheticOptionsProfile = { + burstRunRange: [number, number]; + scenarios: Scenario[]; + pricePlacements: Record[]>; }; export type SyntheticContractIvState = { @@ -36,53 +103,23 @@ export type SyntheticContractIvState = { lastTs: number; }; +export type PricePlacement = "AA" | "A" | "MID" | "B" | "BB"; +export type SyntheticScenarioLabel = SmartMoneyProfileId | "neutral_noise"; +export type SyntheticSmartMoneyScenario = { + id: string; + label: SyntheticScenarioLabel; + hiddenLabel: string; +}; + const OPTION_CONTRACT_MULTIPLIER = 100; const IV_MIN = 0.05; const IV_MAX = 2.5; const IV_DECAY_HALF_LIFE_MS = 60_000; - -const SYNTHETIC_SYMBOLS = ["SPY", ...(SP500_SYMBOLS as readonly string[])]; const MS_PER_DAY = 24 * 60 * 60 * 1000; const EXPIRY_OFFSETS = [0, 1, 7, 14, 28, 45, 60, 90]; const EXCHANGES = ["CBOE", "PHLX", "ISE", "ARCA", "BOX", "MIAX"]; const CONDITIONS = ["SWEEP", "ISO", "FILL", "TEST"]; -type SyntheticOptionsProfile = { - burstRunRange: [number, number]; - scenarios: Scenario[]; - pricePlacements: Record[]>; -}; - -export type PricePlacement = "AA" | "A" | "MID" | "B" | "BB"; - -type WeightedValue = { - value: T; - weight: number; -}; - -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 SYNTHETIC_SYMBOLS = ["SPY", ...(SP500_SYMBOLS as readonly string[])]; const SMART_MONEY_SCENARIO_IDS = [ "institutional_directional", "retail_whale", @@ -93,535 +130,660 @@ const SMART_MONEY_SCENARIO_IDS = [ "neutral_noise" ] as const; -const REALISTIC_SCENARIOS: Scenario[] = [ +const SCENARIO_LIBRARY: 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, + id: "call_sweep", + hiddenLabel: "call_sweep", label: "institutional_directional", right: "C", - countRange: [2, 3], - sizeRange: [180, 520], - targetNotionalRange: [25_000, 90_000], + weight: 1.2, + countRange: [4, 7], + sizeRange: [420, 1200], + targetNotionalRange: [55_000, 165_000], priceTrend: "up", + expiryOffsets: [7, 14, 28], + strikeMoneyness: 1.01, + placementProfile: "institutional_directional", + conditions: ["SWEEP"], flowFeatures: { - nbbo_coverage_ratio: 0.9, - nbbo_aggressive_ratio: 0.82, - nbbo_aggressive_buy_ratio: 0.78, + nbbo_aggressive_ratio: 0.84, + nbbo_aggressive_buy_ratio: 0.8, nbbo_aggressive_sell_ratio: 0.04, nbbo_inside_ratio: 0.08, venue_count: 4 - }, - conditions: ["SWEEP"] + } }, { - id: "bearish_sweep", - weight: 8, + id: "put_sweep", + hiddenLabel: "put_sweep", label: "institutional_directional", right: "P", - countRange: [2, 3], - sizeRange: [180, 520], - targetNotionalRange: [25_000, 90_000], + weight: 1.15, + countRange: [4, 7], + sizeRange: [420, 1200], + targetNotionalRange: [55_000, 165_000], priceTrend: "up", + expiryOffsets: [7, 14, 28], + strikeMoneyness: 0.99, + placementProfile: "institutional_directional", + conditions: ["SWEEP"], flowFeatures: { - nbbo_coverage_ratio: 0.9, - nbbo_aggressive_ratio: 0.82, - nbbo_aggressive_buy_ratio: 0.78, + nbbo_aggressive_ratio: 0.84, + nbbo_aggressive_buy_ratio: 0.8, nbbo_aggressive_sell_ratio: 0.04, nbbo_inside_ratio: 0.08, venue_count: 4 - }, - conditions: ["SWEEP"] + } }, { - id: "contract_spike", - weight: 6, - label: "retail_whale", + id: "ask_lift_accumulation", + hiddenLabel: "ask_lift_accumulation", + label: "institutional_directional", right: "either", - countRange: [2, 3], - sizeRange: [500, 900], - targetNotionalRange: [18_000, 70_000], + weight: 0.95, + countRange: [2, 4], + sizeRange: [160, 540], + targetNotionalRange: [12_000, 50_000], priceTrend: "flat", - expiryOffsets: [0, 1, 7], - strikeMoneyness: 1.08, + strikeMoneyness: 1.0, + placementProfile: "institutional_directional", + conditions: ["FILL"], flowFeatures: { - nbbo_coverage_ratio: 0.76, - nbbo_aggressive_ratio: 0.68, + nbbo_aggressive_ratio: 0.66, nbbo_aggressive_buy_ratio: 0.62, nbbo_aggressive_sell_ratio: 0.08, - nbbo_inside_ratio: 0.12, - execution_iv_shock: 0.16, + nbbo_inside_ratio: 0.14, + venue_count: 2 + } + }, + { + id: "far_dated_conviction", + hiddenLabel: "far_dated_conviction", + label: "institutional_directional", + right: "either", + weight: 0.72, + countRange: [2, 3], + sizeRange: [220, 700], + targetNotionalRange: [35_000, 90_000], + priceTrend: "up", + expiryOffsets: [60, 90], + strikeMoneyness: 1.0, + placementProfile: "institutional_directional", + conditions: ["FILL"], + flowFeatures: { + nbbo_aggressive_ratio: 0.62, + nbbo_aggressive_buy_ratio: 0.56, + nbbo_aggressive_sell_ratio: 0.12, + nbbo_inside_ratio: 0.18, 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"] - } -]; - -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, + id: "0dte_call_chase", + hiddenLabel: "0dte_call_chase", label: "retail_whale", - right: "either", - countRange: [5, 8], - sizeRange: [1200, 3200], - targetNotionalRange: [60_000, 140_000], - priceTrend: "flat", - expiryOffsets: [0, 1, 7], + right: "C", + weight: 1.2, + countRange: [6, 10], + sizeRange: [500, 1400], + targetNotionalRange: [28_000, 90_000], + priceTrend: "up", + expiryOffsets: [0, 1], strikeMoneyness: 1.08, + placementProfile: "retail_whale", + conditions: ["ISO"], 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, + nbbo_inside_ratio: 0.1, + execution_iv_shock: 0.18, venue_count: 4 - }, - conditions: ["ISO"] + } }, { - id: "event_driven", - weight: 12, + id: "short_dated_put_panic", + hiddenLabel: "short_dated_put_panic", + label: "retail_whale", + right: "P", + weight: 0.92, + countRange: [5, 8], + sizeRange: [420, 1200], + targetNotionalRange: [24_000, 82_000], + priceTrend: "up", + expiryOffsets: [0, 1, 7], + strikeMoneyness: 0.94, + placementProfile: "retail_whale", + conditions: ["ISO"], + flowFeatures: { + nbbo_aggressive_ratio: 0.72, + nbbo_aggressive_buy_ratio: 0.64, + nbbo_aggressive_sell_ratio: 0.06, + nbbo_inside_ratio: 0.12, + execution_iv_shock: 0.16, + venue_count: 4 + } + }, + { + id: "attention_contract_spike", + hiddenLabel: "attention_contract_spike", + label: "retail_whale", + right: "either", + weight: 0.84, + countRange: [3, 6], + sizeRange: [360, 900], + targetNotionalRange: [18_000, 60_000], + priceTrend: "flat", + expiryOffsets: [1, 7], + strikeMoneyness: 1.06, + placementProfile: "retail_whale", + conditions: ["ISO"], + flowFeatures: { + nbbo_aggressive_ratio: 0.62, + nbbo_aggressive_buy_ratio: 0.56, + nbbo_aggressive_sell_ratio: 0.08, + nbbo_inside_ratio: 0.14, + execution_iv_shock: 0.14, + venue_count: 3 + } + }, + { + id: "earnings_vol_probe", + hiddenLabel: "earnings_vol_probe", label: "event_driven", right: "C", - countRange: [1, 2], - sizeRange: [700, 1100], - targetNotionalRange: [72_000, 88_000], + weight: 0.9, + countRange: [2, 4], + sizeRange: [180, 520], + targetNotionalRange: [18_000, 52_000], priceTrend: "flat", - expiryOffsets: [28, 45], - strikeMoneyness: 1.0, + expiryOffsets: [14, 28], + strikeMoneyness: 1.03, + preferredSymbols: ["AAPL", "MSFT", "NVDA", "META", "AMZN", "TSLA"], + placementProfile: "event_driven", + conditions: ["FILL", "EVENT_14D"], 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, + nbbo_aggressive_ratio: 0.46, + nbbo_aggressive_buy_ratio: 0.42, + nbbo_aggressive_sell_ratio: 0.12, + nbbo_inside_ratio: 0.2, venue_count: 2 - }, - conditions: ["FILL"] + } }, { - id: "vol_seller", - weight: 12, + id: "pre_event_directional_ramp", + hiddenLabel: "pre_event_directional_ramp", + label: "event_driven", + right: "C", + weight: 1.1, + countRange: [4, 7], + sizeRange: [380, 920], + targetNotionalRange: [46_000, 120_000], + priceTrend: "up", + expiryOffsets: [7, 14], + strikeMoneyness: 1.02, + preferredSymbols: ["AAPL", "MSFT", "NVDA", "META", "AMZN", "TSLA"], + placementProfile: "event_driven", + conditions: ["FILL", "EVENT_14D"], + flowFeatures: { + corporate_event_ts_offset_days: 7, + nbbo_aggressive_ratio: 0.62, + nbbo_aggressive_buy_ratio: 0.58, + nbbo_aggressive_sell_ratio: 0.08, + nbbo_inside_ratio: 0.14, + venue_count: 3 + } + }, + { + id: "post_gap_followthrough", + hiddenLabel: "post_gap_followthrough", + label: "event_driven", + right: "either", + weight: 0.88, + countRange: [3, 5], + sizeRange: [260, 760], + targetNotionalRange: [24_000, 68_000], + priceTrend: "up", + expiryOffsets: [7, 14], + strikeMoneyness: 1.0, + preferredSymbols: ["AAPL", "MSFT", "NVDA", "META", "AMZN", "TSLA"], + placementProfile: "event_driven", + conditions: ["FILL", "EVENT_14D"], + flowFeatures: { + corporate_event_ts_offset_days: 1, + nbbo_aggressive_ratio: 0.58, + nbbo_aggressive_buy_ratio: 0.52, + nbbo_aggressive_sell_ratio: 0.1, + nbbo_inside_ratio: 0.16, + venue_count: 3 + } + }, + { + id: "covered_call_overwrite", + hiddenLabel: "covered_call_overwrite", + label: "vol_seller", + right: "C", + weight: 0.82, + countRange: [3, 5], + sizeRange: [700, 1800], + targetNotionalRange: [55_000, 150_000], + priceTrend: "down", + expiryOffsets: [28, 45, 60], + strikeMoneyness: 1.06, + placementProfile: "vol_seller", + conditions: ["FILL"], + flowFeatures: { + nbbo_aggressive_ratio: 0.54, + nbbo_aggressive_buy_ratio: 0.08, + nbbo_aggressive_sell_ratio: 0.52, + nbbo_inside_ratio: 0.16, + venue_count: 2 + } + }, + { + id: "cash_secured_put_write", + hiddenLabel: "cash_secured_put_write", + label: "vol_seller", + right: "P", + weight: 0.82, + countRange: [3, 5], + sizeRange: [700, 1800], + targetNotionalRange: [55_000, 150_000], + priceTrend: "down", + expiryOffsets: [28, 45, 60], + strikeMoneyness: 0.96, + placementProfile: "vol_seller", + conditions: ["FILL"], + flowFeatures: { + nbbo_aggressive_ratio: 0.54, + nbbo_aggressive_buy_ratio: 0.08, + nbbo_aggressive_sell_ratio: 0.52, + nbbo_inside_ratio: 0.16, + venue_count: 2 + } + }, + { + id: "short_straddle_harvest", + hiddenLabel: "short_straddle_harvest", label: "vol_seller", right: "either", - countRange: [4, 6], - sizeRange: [1300, 2100], - targetNotionalRange: [150_000, 210_000], + weight: 1.15, + countRange: [4, 7], + sizeRange: [650, 1500], + targetNotionalRange: [60_000, 150_000], priceTrend: "down", expiryOffsets: [28, 45], strikeMoneyness: 1.0, + placementProfile: "vol_seller", + conditions: ["FILL"], + legs: [ + { right: "C", strikeMoneyness: 1.0, placementScenarioId: "vol_seller" }, + { right: "P", strikeMoneyness: 1.0, placementScenarioId: "vol_seller" } + ], flowFeatures: { structure_type: "straddle", structure_legs: 2, structure_strikes: 1, - structure_rights: "CP", + structure_rights: "C/P", conditions: "COMPLEX", - nbbo_coverage_ratio: 0.9, - nbbo_aggressive_ratio: 0.72, + nbbo_aggressive_ratio: 0.7, nbbo_aggressive_buy_ratio: 0.08, - nbbo_aggressive_sell_ratio: 0.7, - nbbo_inside_ratio: 0.1, - same_size_leg_symmetry: 0.66, + nbbo_aggressive_sell_ratio: 0.68, + nbbo_inside_ratio: 0.12, + same_size_leg_symmetry: 0.9, venue_count: 3 - }, - conditions: ["FILL"] + } }, { - id: "arbitrage", - weight: 12, + id: "parity_vertical", + hiddenLabel: "parity_vertical", label: "arbitrage", - right: "either", - countRange: [4, 6], - sizeRange: [900, 1400], - targetNotionalRange: [70_000, 115_000], + right: "C", + weight: 1.0, + countRange: [4, 7], + sizeRange: [520, 1400], + targetNotionalRange: [45_000, 120_000], priceTrend: "flat", expiryOffsets: [28, 45], - strikeMoneyness: 1.0, + placementProfile: "arbitrage", + conditions: ["FILL"], + legs: [ + { right: "C", strikeOffsetSteps: -1, placementScenarioId: "arbitrage" }, + { right: "C", strikeOffsetSteps: 1, placementScenarioId: "arbitrage" } + ], 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, + structure_rights: "C", + nbbo_aggressive_ratio: 0.38, nbbo_aggressive_buy_ratio: 0.42, nbbo_aggressive_sell_ratio: 0.38, - nbbo_inside_ratio: 0.32, - same_size_leg_symmetry: 0.92, + nbbo_inside_ratio: 0.3, + same_size_leg_symmetry: 0.94, venue_count: 3 - }, - conditions: ["FILL"] + } }, { - id: "hedge_reactive", - weight: 12, + id: "conversion_reversal", + hiddenLabel: "conversion_reversal", + label: "arbitrage", + right: "either", + weight: 0.76, + countRange: [5, 8], + sizeRange: [420, 1100], + targetNotionalRange: [38_000, 95_000], + priceTrend: "flat", + expiryOffsets: [28, 45], + placementProfile: "arbitrage", + conditions: ["FILL"], + flowFeatures: { + structure_type: "roll", + structure_legs: 3, + structure_strikes: 2, + structure_rights: "C/P", + nbbo_aggressive_ratio: 0.32, + nbbo_aggressive_buy_ratio: 0.34, + nbbo_aggressive_sell_ratio: 0.32, + nbbo_inside_ratio: 0.34, + same_size_leg_symmetry: 0.9, + venue_count: 3 + } + }, + { + id: "box_spread", + hiddenLabel: "box_spread", + label: "arbitrage", + right: "either", + weight: 0.66, + countRange: [6, 10], + sizeRange: [300, 900], + targetNotionalRange: [26_000, 80_000], + priceTrend: "flat", + expiryOffsets: [28, 45], + placementProfile: "arbitrage", + conditions: ["FILL"], + flowFeatures: { + structure_type: "box", + structure_legs: 4, + structure_strikes: 2, + structure_rights: "C/P", + nbbo_aggressive_ratio: 0.24, + nbbo_aggressive_buy_ratio: 0.26, + nbbo_aggressive_sell_ratio: 0.24, + nbbo_inside_ratio: 0.42, + same_size_leg_symmetry: 0.94, + venue_count: 2 + } + }, + { + id: "gamma_pinch_call_hedge", + hiddenLabel: "gamma_pinch_call_hedge", label: "hedge_reactive", - right: "P", - countRange: [1, 2], - sizeRange: [2600, 3400], - targetNotionalRange: [35_000, 50_000], + right: "C", + weight: 0.92, + countRange: [4, 7], + sizeRange: [900, 2400], + targetNotionalRange: [30_000, 85_000], priceTrend: "up", expiryOffsets: [0, 1], strikeMoneyness: 1.0, + preferredSymbols: ["SPY", "QQQ", "IWM", "AAPL", "NVDA"], + placementProfile: "hedge_reactive", + conditions: ["FILL"], flowFeatures: { - nbbo_coverage_ratio: 0.86, nbbo_aggressive_ratio: 0.58, nbbo_aggressive_buy_ratio: 0.54, - nbbo_aggressive_sell_ratio: 0.12, + nbbo_aggressive_sell_ratio: 0.1, nbbo_inside_ratio: 0.16, - underlying_move_bps: -72, + underlying_move_bps: 44, venue_count: 3 - }, - conditions: ["FILL"] + } }, { - id: "neutral_noise", - weight: 20, + id: "reactive_put_wall", + hiddenLabel: "reactive_put_wall", + label: "hedge_reactive", + right: "P", + weight: 1.15, + countRange: [4, 7], + sizeRange: [1200, 2600], + targetNotionalRange: [35_000, 90_000], + priceTrend: "up", + expiryOffsets: [0, 1], + strikeMoneyness: 1.0, + preferredSymbols: ["SPY", "QQQ", "IWM", "AAPL", "NVDA"], + placementProfile: "hedge_reactive", + conditions: ["FILL"], + flowFeatures: { + nbbo_aggressive_ratio: 0.56, + nbbo_aggressive_buy_ratio: 0.54, + nbbo_aggressive_sell_ratio: 0.1, + nbbo_inside_ratio: 0.16, + underlying_move_bps: -64, + venue_count: 3 + } + }, + { + id: "dealer_unwind", + hiddenLabel: "dealer_unwind", + label: "hedge_reactive", + right: "either", + weight: 0.88, + countRange: [3, 6], + sizeRange: [700, 2000], + targetNotionalRange: [26_000, 72_000], + priceTrend: "down", + expiryOffsets: [0, 1, 7], + strikeMoneyness: 1.0, + preferredSymbols: ["SPY", "QQQ", "IWM", "AAPL", "NVDA"], + placementProfile: "hedge_reactive", + conditions: ["FILL"], + flowFeatures: { + nbbo_aggressive_ratio: 0.5, + nbbo_aggressive_buy_ratio: 0.18, + nbbo_aggressive_sell_ratio: 0.44, + nbbo_inside_ratio: 0.18, + underlying_move_bps: -28, + venue_count: 3 + } + }, + { + id: "single_print_mid", + hiddenLabel: "single_print_mid", label: "neutral_noise", right: "either", + weight: 1.2, countRange: [1, 2], - sizeRange: [10, 70], + sizeRange: [8, 60], + targetNotionalRange: [500, 5_000], + priceTrend: "flat", + strikeMoneyness: 1.0, + placementProfile: "neutral_noise", + conditions: ["FILL"], + flowFeatures: { + nbbo_aggressive_ratio: 0.18, + nbbo_aggressive_buy_ratio: 0.16, + nbbo_aggressive_sell_ratio: 0.12, + nbbo_inside_ratio: 0.62, + venue_count: 1 + } + }, + { + id: "two_sided_scalp", + hiddenLabel: "two_sided_scalp", + label: "neutral_noise", + right: "either", + weight: 1.0, + countRange: [2, 4], + sizeRange: [10, 120], targetNotionalRange: [800, 7_000], priceTrend: "flat", - expiryOffsets: [14, 28, 45, 60], - strikeMoneyness: 1.02, + strikeMoneyness: 1.0, + placementProfile: "neutral_noise", + conditions: ["FILL"], flowFeatures: { - nbbo_coverage_ratio: 0.78, - nbbo_aggressive_ratio: 0.22, + nbbo_aggressive_ratio: 0.24, nbbo_aggressive_buy_ratio: 0.22, - nbbo_aggressive_sell_ratio: 0.18, + nbbo_aggressive_sell_ratio: 0.2, + nbbo_inside_ratio: 0.54, + venue_count: 2 + } + }, + { + id: "stale_quote_noise", + hiddenLabel: "stale_quote_noise", + label: "neutral_noise", + right: "either", + weight: 0.86, + countRange: [1, 3], + sizeRange: [8, 80], + targetNotionalRange: [600, 5_500], + priceTrend: "flat", + strikeMoneyness: 1.0, + placementProfile: "neutral_noise", + missingQuoteProbability: 0.12, + staleQuoteProbability: 0.44, + conditions: ["TEST"], + flowFeatures: { + nbbo_aggressive_ratio: 0.16, + nbbo_aggressive_buy_ratio: 0.16, + nbbo_aggressive_sell_ratio: 0.12, nbbo_inside_ratio: 0.58, venue_count: 1 - }, - conditions: ["FILL"] + } } ]; -const REALISTIC_PRICE_PLACEMENTS: Record[]> = { - ask_lift: [ - { value: "A", weight: 45 }, - { value: "AA", weight: 20 }, - { value: "MID", weight: 25 }, - { value: "B", weight: 8 }, - { value: "BB", weight: 2 } +const PLACEMENTS: Record[]> = { + institutional_directional: [ + { value: "AA", weight: 18 }, + { value: "A", weight: 44 }, + { value: "MID", weight: 18 }, + { value: "B", weight: 14 }, + { value: "BB", weight: 6 } ], - mid_block: [ - { value: "MID", weight: 60 }, - { value: "A", weight: 20 }, - { value: "B", weight: 20 } - ], - bullish_sweep: [ - { value: "AA", weight: 20 }, - { value: "A", weight: 50 }, - { value: "MID", weight: 15 }, - { value: "B", weight: 10 }, - { value: "BB", weight: 5 } - ], - bearish_sweep: [ - { value: "AA", weight: 10 }, - { value: "A", weight: 20 }, - { value: "MID", weight: 15 }, - { value: "B", weight: 35 }, - { value: "BB", weight: 20 } - ], - contract_spike: [ - { value: "A", weight: 25 }, - { value: "MID", weight: 40 }, - { value: "B", weight: 25 }, - { value: "AA", weight: 5 }, - { value: "BB", weight: 5 } - ], - noise: [ - { value: "MID", weight: 40 }, - { value: "A", weight: 20 }, + retail_whale: [ + { value: "AA", weight: 14 }, + { value: "A", weight: 30 }, + { value: "MID", weight: 24 }, { value: "B", weight: 20 }, + { value: "BB", weight: 12 } + ], + event_driven: [ + { value: "AA", weight: 12 }, + { value: "A", weight: 34 }, + { value: "MID", weight: 24 }, + { value: "B", weight: 18 }, + { value: "BB", weight: 12 } + ], + vol_seller: [ + { value: "AA", weight: 4 }, + { value: "A", weight: 8 }, + { value: "MID", weight: 22 }, + { value: "B", weight: 36 }, + { value: "BB", weight: 30 } + ], + arbitrage: [ { value: "AA", weight: 10 }, + { value: "A", weight: 18 }, + { value: "MID", weight: 44 }, + { value: "B", weight: 18 }, { value: "BB", weight: 10 } + ], + hedge_reactive: [ + { value: "AA", weight: 16 }, + { value: "A", weight: 28 }, + { value: "MID", weight: 18 }, + { value: "B", weight: 24 }, + { value: "BB", weight: 14 } + ], + neutral_noise: [ + { value: "AA", weight: 8 }, + { value: "A", weight: 14 }, + { value: "MID", weight: 44 }, + { value: "B", weight: 22 }, + { value: "BB", weight: 12 } ] }; -const ACTIVE_PRICE_PLACEMENTS: Record[]> = { - bullish_sweep: [ - { value: "AA", weight: 25 }, - { value: "A", weight: 40 }, - { value: "B", weight: 20 }, - { value: "BB", weight: 15 } - ], - bearish_sweep: [ - { value: "AA", weight: 15 }, - { value: "A", weight: 20 }, - { value: "B", weight: 40 }, - { value: "BB", weight: 25 } - ], - contract_spike: [ - { value: "AA", weight: 25 }, - { value: "A", weight: 25 }, - { value: "B", weight: 25 }, - { value: "BB", weight: 25 } - ], - noise: [ - { value: "AA", weight: 25 }, - { value: "A", weight: 25 }, - { value: "B", weight: 25 }, - { value: "BB", weight: 25 } - ] -}; - -const FIREHOSE_PRICE_PLACEMENTS: Record[]> = { - ...ACTIVE_PRICE_PLACEMENTS, - noise: [ - { value: "A", weight: 20 }, - { value: "AA", weight: 20 }, - { value: "MID", weight: 20 }, - { value: "B", weight: 20 }, - { value: "BB", weight: 20 } - ] -}; - -const PLACEMENT_PATTERN: PricePlacement[] = ["A", "AA", "MID", "B", "BB"]; - const SYNTHETIC_PROFILES: Record = { realistic: { - burstRunRange: [1, 2], - scenarios: REALISTIC_SCENARIOS, - pricePlacements: REALISTIC_PRICE_PLACEMENTS + burstRunRange: [1, 1], + scenarios: SCENARIO_LIBRARY.map((scenario) => ({ + ...scenario, + countRange: [scenario.countRange[0], scenario.countRange[1]], + sizeRange: [scenario.sizeRange[0], scenario.sizeRange[1]], + targetNotionalRange: [ + scenario.targetNotionalRange[0], + scenario.targetNotionalRange[1] + ] + })), + pricePlacements: PLACEMENTS }, active: { - burstRunRange: [2, 4], - scenarios: ACTIVE_SCENARIOS, - pricePlacements: ACTIVE_PRICE_PLACEMENTS + burstRunRange: [1, 2], + scenarios: SCENARIO_LIBRARY.map((scenario) => ({ + ...scenario, + countRange: [scenario.countRange[0] + 1, scenario.countRange[1] + 2], + sizeRange: [ + Math.round(scenario.sizeRange[0] * 1.4), + Math.round(scenario.sizeRange[1] * 1.55) + ], + targetNotionalRange: [ + Math.round(scenario.targetNotionalRange[0] * 1.35), + Math.round(scenario.targetNotionalRange[1] * 1.55) + ] + })), + pricePlacements: PLACEMENTS }, firehose: { - burstRunRange: [4, 7], - scenarios: ACTIVE_SCENARIOS.map((scenario): Scenario => - scenario.id === "noise" - ? { - ...scenario, - weight: 20, - countRange: [5, 8], - sizeRange: [20, 300], - targetNotionalRange: [800, 12_000] - } - : { - ...scenario, - weight: scenario.weight + 10, - countRange: [scenario.countRange[0] + 2, scenario.countRange[1] + 3], - sizeRange: [scenario.sizeRange[0], scenario.sizeRange[1] * 2], - targetNotionalRange: [ - scenario.targetNotionalRange[0], - scenario.targetNotionalRange[1] * 1.5 - ] - } - ), - pricePlacements: FIREHOSE_PRICE_PLACEMENTS + burstRunRange: [2, 3], + scenarios: SCENARIO_LIBRARY.map((scenario) => ({ + ...scenario, + countRange: [scenario.countRange[0] + 2, scenario.countRange[1] + 4], + sizeRange: [ + Math.round(scenario.sizeRange[0] * 1.8), + Math.round(scenario.sizeRange[1] * 2.1) + ], + targetNotionalRange: [ + Math.round(scenario.targetNotionalRange[0] * 1.7), + Math.round(scenario.targetNotionalRange[1] * 2.0) + ] + })), + pricePlacements: PLACEMENTS } }; -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 SMART_MONEY_TEMPLATE_SCENARIOS: Record< + Exclude<(typeof SMART_MONEY_SCENARIO_IDS)[number], "neutral_noise">, + string +> = { + institutional_directional: "call_sweep", + retail_whale: "0dte_call_chase", + event_driven: "pre_event_directional_ramp", + vol_seller: "short_straddle_harvest", + arbitrage: "parity_vertical", + hedge_reactive: "reactive_put_wall" }; -const pick = (items: T[], seed: number): T => { - return items[Math.abs(seed) % items.length]; +const pick = (items: readonly T[], seed: number): T => { + return items[Math.abs(seed) % items.length]!; }; const pickInt = (min: number, max: number, seed: number): number => { if (max <= min) { return min; } - const span = max - min + 1; - return min + (Math.abs(seed) % span); + return min + (Math.abs(seed) % (max - min + 1)); }; const pickFloat = (min: number, max: number, seed: number): number => { if (max <= min) { return min; } - const offset = (Math.abs(seed) % 1000) / 1000; - return min + (max - min) * offset; + return min + (max - min) * ((Math.abs(seed) % 1000) / 1000); }; const pickWeighted = (items: T[], seed: number): T => { @@ -633,42 +795,22 @@ const pickWeighted = (items: T[], seed: number): T } target -= item.weight; } - return items[0]; + return items[0]!; }; const pickWeightedValue = (items: WeightedValue[], seed: number): T => { - return pickWeighted(items, seed).value; -}; - -const pickPlacement = ( - burst: Burst, - index: number, - profile: SyntheticOptionsProfile -): PricePlacement => { - const placementOptions = profile.pricePlacements[burst.scenarioId] ?? profile.pricePlacements.noise; - const offset = Math.abs(burst.seed) % PLACEMENT_PATTERN.length; - if (index < PLACEMENT_PATTERN.length) { - return PLACEMENT_PATTERN[(offset + index) % PLACEMENT_PATTERN.length]; - } - return pickWeightedValue(placementOptions, burst.seed + index * 11); -}; - -const hashSymbol = (value: string): number => { - let hash = 0; - for (let i = 0; i < value.length; i += 1) { - hash = (hash * 31 + value.charCodeAt(i)) >>> 0; - } - return hash; + return pickWeighted( + items.map((item) => ({ ...item })), + seed + ).value; }; const formatStrike = (strike: number): string => { - const fixed = strike.toFixed(3); - return fixed.replace(/\.?0+$/, ""); + return strike.toFixed(3).replace(/\.?0+$/, ""); }; const formatExpiry = (now: number, offsetDays: number): string => { - const expiryDate = new Date(now + offsetDays * MS_PER_DAY); - return expiryDate.toISOString().slice(0, 10); + return new Date(now + offsetDays * MS_PER_DAY).toISOString().slice(0, 10); }; const clampValue = (value: number, min: number, max: number): number => { @@ -707,7 +849,10 @@ export const updateSyntheticIvForTest = ( if (input.placement === "AA" || input.placement === "A") { const sizeImpact = Math.log10(Math.max(10, input.size)) * 0.012; const notionalImpact = Math.log10(Math.max(1_000, input.notional)) * 0.01; - pressure += input.placement === "AA" ? sizeImpact + notionalImpact : (sizeImpact + notionalImpact) * 0.65; + pressure += + input.placement === "AA" + ? sizeImpact + notionalImpact + : (sizeImpact + notionalImpact) * 0.65; } else if (input.placement === "MID") { pressure += 0.001; } else { @@ -720,115 +865,423 @@ export const updateSyntheticIvForTest = ( return { iv: Number(iv.toFixed(4)), pressure, lastTs: input.ts }; }; -const buildBurst = (burstIndex: number, now: number, profile: SyntheticOptionsProfile): Burst => { - const symbol = SYNTHETIC_SYMBOLS[burstIndex % SYNTHETIC_SYMBOLS.length]; - const symbolHash = hashSymbol(symbol); - const seed = symbolHash + burstIndex * 7; - const scenario = pickWeighted(profile.scenarios, seed); - const baseUnderlying = 30 + (symbolHash % 470); - 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, - templateStrike ?? Math.round(baseUnderlying / strikeStep) * strikeStep + strikeOffset * strikeStep +const estimateSyntheticOptionMid = (input: { + underlying: number; + strike: number; + right: "C" | "P"; + dteDays: number; + moneyness: number; + mode: SyntheticMarketMode; +}): number => { + const intrinsic = + input.right === "C" + ? Math.max(0, input.underlying - input.strike) + : Math.max(0, input.strike - input.underlying); + const timeYears = Math.max(1, input.dteDays + 1) / 365; + const baselineIv = initializeSyntheticIv(input.dteDays, input.moneyness); + const modeBoost = + input.mode === "firehose" ? 1.18 : input.mode === "active" ? 1.08 : 0.96; + const distance = Math.abs(input.moneyness - 1); + const extrinsic = + input.underlying * + baselineIv * + Math.sqrt(timeYears) * + Math.exp(-distance * 5.4) * + 0.72 * + modeBoost; + const skewBoost = input.right === "P" && input.moneyness >= 1 ? 1.06 : 1; + return Number( + clampValue(intrinsic + extrinsic * skewBoost, 0.05, input.underlying * 0.45).toFixed(2) ); +}; + +const createCoverageWindowState = (): CoverageWindowState => ({ + institutional_directional: [], + retail_whale: [], + event_driven: [], + vol_seller: [], + arbitrage: [], + hedge_reactive: [] +}); + +const burstSequenceCache = new Map(); + +const getCoverageCounts = ( + coverageState: CoverageWindowState, + now: number, + control: SyntheticControlState +) => { + const floorTs = now - control.coverage_window_minutes * 60_000; + const counts = buildEmptySyntheticProfileHitCounts(); + for (const profileId of Object.keys(coverageState) as SmartMoneyProfileId[]) { + coverageState[profileId] = coverageState[profileId].filter((ts) => ts >= floorTs); + counts[profileId] = coverageState[profileId].length; + } + return counts; +}; + +const recordCoverageHit = ( + coverageState: CoverageWindowState, + profileId: SyntheticScenarioLabel, + now: number +) => { + if (profileId === "neutral_noise") { + return; + } + coverageState[profileId].push(now); +}; + +const chooseScenario = ( + profile: SyntheticOptionsProfile, + now: number, + control: SyntheticControlState, + coverageState: CoverageWindowState +): Scenario => { + const session = getSyntheticSessionState(now, control); + const focusSymbol = session.focus_symbols[0] ?? SYNTHETIC_SYMBOLS[0]!; + const familyWeights = getSyntheticScenarioWeights( + focusSymbol, + now, + control, + session + ); + const coverageCounts = getCoverageCounts(coverageState, now, control); + const weightedScenarios = profile.scenarios.map((scenario, index) => { + const familyWeight = familyWeights[scenario.label]; + const coverageBoost = + scenario.label === "neutral_noise" + ? 1 + : getSyntheticCoverageBoost( + scenario.label, + { profile_hit_counts: coverageCounts }, + control + ); + const quietBias = + scenario.label === "neutral_noise" && index % 2 === 0 + ? 1.08 + : scenario.label === "neutral_noise" + ? 0.94 + : 1; + return { + ...scenario, + weight: Math.max(1, Math.round(scenario.weight * familyWeight * coverageBoost * quietBias * 100)) + }; + }); + return pickWeighted(weightedScenarios, now + control.shared_seed * 31); +}; + +const pickScenarioSymbol = ( + scenario: Scenario, + now: number, + control: SyntheticControlState +): string => { + const session = getSyntheticSessionState(now, control); + const symbolPool = + scenario.preferredSymbols?.length && (scenario.label === "event_driven" || Math.abs(now) % 4 === 0) + ? [...scenario.preferredSymbols] + : session.focus_symbols.length > 0 + ? [...session.focus_symbols, ...SYNTHETIC_SYMBOLS] + : [...SYNTHETIC_SYMBOLS]; + return pick(symbolPool, hashSyntheticSymbol(scenario.id) + session.seed_bucket); +}; + +const buildDynamicFlowFeatures = ( + scenario: Scenario, + symbol: string, + now: number, + control: SyntheticControlState +): FlowPacket["features"] => { + const session = getSyntheticSessionState(now, control); + const underlying = getSyntheticUnderlyingState(symbol, now, control, session); + const baseCoverage = 0.76 + session.quote_cleanliness * 0.18; + const baseSpreadZ = clampValue( + (underlying.spread / Math.max(0.01, underlying.mid)) * 650, + 0.04, + 0.34 + ); + const eventOffset = + scenario.label === "event_driven" + ? Number(scenario.flowFeatures.corporate_event_ts_offset_days ?? 7) + : 0; + return { + ...scenario.flowFeatures, + nbbo_coverage_ratio: clampValue( + Math.max( + Number(scenario.flowFeatures.nbbo_coverage_ratio ?? 0), + baseCoverage - (scenario.missingQuoteProbability ?? 0) * 0.45 + ), + 0.3, + 0.96 + ), + nbbo_inside_ratio: clampValue( + Number(scenario.flowFeatures.nbbo_inside_ratio ?? 0.2) + + (session.regime === "arb_calm" ? 0.08 : 0) - + (session.regime === "event_ramp" ? 0.04 : 0), + 0.04, + 0.72 + ), + nbbo_spread_z: clampValue( + Math.max(Number(scenario.flowFeatures.nbbo_spread_z ?? 0), baseSpreadZ), + 0.02, + 0.4 + ), + execution_iv_shock: clampValue( + Math.max( + Number(scenario.flowFeatures.execution_iv_shock ?? 0), + session.volatility_level * 0.12 + (scenario.label === "retail_whale" ? 0.04 : 0) + ), + 0, + 0.26 + ), + underlying_move_bps: Math.round( + (Number(scenario.flowFeatures.underlying_move_bps ?? underlying.driftBps) + + underlying.shockBps * 0.35) * + 100 + ) / 100, + venue_count: Math.max( + 1, + Math.round( + Number(scenario.flowFeatures.venue_count ?? 1) + + (session.regime === "event_ramp" ? 1 : 0) + + (session.regime === "dealer_gamma" ? 1 : 0) + ) + ), + ...(eventOffset > 0 ? { corporate_event_ts_offset_days: eventOffset } : {}) + }; +}; + +const buildBurst = ( + burstIndex: number, + now: number, + mode: SyntheticMarketMode, + profile: SyntheticOptionsProfile, + control: SyntheticControlState, + coverageState: CoverageWindowState, + scenarioOverride?: Scenario +): Burst => { + const scenario = + scenarioOverride ?? chooseScenario(profile, now, control, coverageState); + const symbol = pickScenarioSymbol(scenario, now, control); + const symbolHash = hashSyntheticSymbol(symbol); + const seed = symbolHash + burstIndex * 7; + const session = getSyntheticSessionState(now, control); + const underlyingState = getSyntheticUnderlyingState(symbol, now, control, session); + const baseUnderlying = underlyingState.mid; + const expiryOffset = pick( + scenario.expiryOffsets ?? EXPIRY_OFFSETS, + symbolHash + burstIndex + ); + const strikeStep = baseUnderlying >= 200 ? 10 : baseUnderlying >= 100 ? 5 : 2.5; const right = scenario.right === "either" ? (symbolHash + burstIndex) % 2 === 0 ? "C" : "P" : scenario.right; - const contractId = `${symbol}-${expiry}-${formatStrike(strike)}-${right}`; - const exchange = pick(EXCHANGES, burstIndex + symbolHash); - const printCount = pickInt(scenario.countRange[0], scenario.countRange[1], symbolHash + burstIndex * 13); - const baseSize = pickInt(scenario.sizeRange[0], scenario.sizeRange[1], symbolHash + burstIndex * 17); + const cycles = pickInt( + scenario.countRange[0], + scenario.countRange[1], + symbolHash + burstIndex * 13 + ); + const baseSize = pickInt( + scenario.sizeRange[0], + scenario.sizeRange[1], + symbolHash + burstIndex * 17 + ); const targetNotional = pickFloat( scenario.targetNotionalRange[0], scenario.targetNotionalRange[1], symbolHash + burstIndex * 19 ); - const basePricePer = Math.max( - 0.05, - Number( - ( - targetNotional / - (baseSize * printCount * OPTION_CONTRACT_MULTIPLIER) - ).toFixed(2) - ) - ); - const conditions = scenario.conditions?.length ? scenario.conditions : [pick(CONDITIONS, burstIndex)]; + const conditions = scenario.conditions?.length + ? [...scenario.conditions] + : [pick(CONDITIONS, burstIndex)]; const priceStep = scenario.priceTrend === "up" ? 0.01 : scenario.priceTrend === "down" ? -0.01 : 0; + const flowFeatures = buildDynamicFlowFeatures(scenario, symbol, now, control); + const legTemplates = + scenario.legs?.length + ? scenario.legs + : [ + { + right, + strikeMoneyness: scenario.strikeMoneyness, + placementScenarioId: scenario.placementProfile ?? scenario.label + } + ]; + const targetNotionalPerLeg = targetNotional / legTemplates.length; + + const legs = legTemplates.map((template, legIndex): BurstLeg => { + const legExpiryOffset = template.expiryOffsetDays ?? expiryOffset; + const expiry = formatExpiry(now, legExpiryOffset); + const moneynessSteps = scenario.label === "neutral_noise" ? 5 : 2; + const strikeOffset = + template.strikeOffsetSteps ?? + pickInt(-moneynessSteps, moneynessSteps, symbolHash + burstIndex * 11 + legIndex * 17); + const templateStrike = + template.strikeMoneyness !== undefined + ? Math.round((baseUnderlying * template.strikeMoneyness) / strikeStep) * strikeStep + : scenario.strikeMoneyness !== undefined + ? Math.round((baseUnderlying * scenario.strikeMoneyness) / strikeStep) * strikeStep + : null; + const strike = Math.max( + 1, + templateStrike ?? + Math.round(baseUnderlying / strikeStep) * strikeStep + + strikeOffset * strikeStep + ); + const legSize = Math.max(1, Math.round(baseSize * (template.sizeMultiplier ?? 1))); + const legMoneyness = strike / baseUnderlying; + const theoreticalMid = estimateSyntheticOptionMid({ + underlying: baseUnderlying, + strike, + right: template.right, + dteDays: legExpiryOffset, + moneyness: legMoneyness, + mode + }); + const targetMid = + targetNotionalPerLeg / + Math.max(1, legSize * cycles * OPTION_CONTRACT_MULTIPLIER); + const cappedTheoreticalMid = Math.min( + theoreticalMid, + Math.max(0.35, targetMid * (scenario.label === "institutional_directional" ? 2.2 : 2.6)) + ); + const blendedMid = cappedTheoreticalMid * 0.45 + targetMid * 0.55 * (template.priceMultiplier ?? 1); + return { + contractId: `${symbol}-${expiry}-${formatStrike(strike)}-${template.right}`, + right: template.right, + expiryOffsetDays: legExpiryOffset, + strike, + basePrice: Number(Math.max(0.05, blendedMid).toFixed(2)), + baseSize: legSize, + exchange: pick(EXCHANGES, burstIndex + symbolHash + legIndex * 3), + placementScenarioId: + template.placementScenarioId ?? scenario.placementProfile ?? scenario.label + }; + }); + + const primaryLeg = legs[0]!; return { - contractId, + contractId: primaryLeg.contractId, underlying: baseUnderlying, - expiryOffsetDays: expiryOffset, - strike, - basePrice: basePricePer, - baseSize, - exchange, + expiryOffsetDays: primaryLeg.expiryOffsetDays, + strike: primaryLeg.strike, + basePrice: primaryLeg.basePrice, + baseSize: primaryLeg.baseSize, + legs, conditions, - printCount, + cycles, + printCount: cycles * legs.length, priceStep, scenarioId: scenario.id, label: scenario.label, - flowFeatures: scenario.flowFeatures, - seed + hiddenLabel: scenario.hiddenLabel, + flowFeatures, + seed, + missingQuoteProbability: + scenario.missingQuoteProbability ?? + clampValue((1 - session.quote_cleanliness) * 0.16, 0, 0.18), + staleQuoteProbability: + scenario.staleQuoteProbability ?? + clampValue((1 - session.quote_cleanliness) * 0.3, 0, 0.42) }; }; -export const buildSyntheticBurstForTest = ( - burstIndex: number, - now: number, - mode: SyntheticMarketMode -): Burst => buildBurst(burstIndex, now, SYNTHETIC_PROFILES[mode]); +const pickPlacement = (burst: Burst, index: number): PricePlacement => { + const key = burst.legs[index % burst.legs.length]?.placementScenarioId ?? burst.label; + const placementOptions = PLACEMENTS[key] ?? PLACEMENTS[burst.label] ?? PLACEMENTS.neutral_noise; + return pickWeightedValue(placementOptions, burst.seed + index * 11); +}; export const listSyntheticSmartMoneyScenariosForTest = (): SyntheticSmartMoneyScenario[] => SMART_MONEY_SCENARIO_IDS.map((id) => ({ id, label: id, - hiddenLabel: id + hiddenLabel: + id === "neutral_noise" + ? "single_print_mid" + : SMART_MONEY_TEMPLATE_SCENARIOS[id as Exclude<(typeof SMART_MONEY_SCENARIO_IDS)[number], "neutral_noise">] })); 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]] - }); + const control = { + preset_id: + scenarioId === "event_driven" + ? "event_day" + : scenarioId === "hedge_reactive" + ? "dealer_day" + : scenarioId === "retail_whale" + ? "retail_chase" + : "balanced_demo", + coverage_assist: true, + coverage_window_minutes: 20, + shared_seed: 11, + profile_weights: { + institutional_directional: 1.0, + retail_whale: 1.0, + event_driven: 1.0, + vol_seller: 1.0, + arbitrage: 1.0, + hedge_reactive: 1.0 + }, + updated_at: 0, + updated_by: "system" + } satisfies SyntheticControlState; + const mode: SyntheticMarketMode = + scenarioId === "retail_whale" || scenarioId === "neutral_noise" + ? "realistic" + : "active"; + const profile = SYNTHETIC_PROFILES[mode]; + const coverageState = createCoverageWindowState(); + const scenario = + scenarioId === "neutral_noise" + ? profile.scenarios.find((candidate) => candidate.id === "single_print_mid")! + : profile.scenarios.find( + (candidate) => candidate.id === SMART_MONEY_TEMPLATE_SCENARIOS[ + scenarioId as Exclude<(typeof SMART_MONEY_SCENARIO_IDS)[number], "neutral_noise"> + ] + )!; + return buildBurst(1, now, mode, profile, control, coverageState, scenario); }; export const buildSyntheticFlowPacketForTest = ( scenarioId: (typeof SMART_MONEY_SCENARIO_IDS)[number], now: number -): { packet: FlowPacket; hiddenLabel: SyntheticScenarioLabel } => { +): { packet: FlowPacket; hiddenLabel: string } => { const burst = buildSyntheticSmartMoneyBurstForTest(scenarioId, now); - const corporateEventOffset = Number(burst.flowFeatures.corporate_event_ts_offset_days ?? 0); + const primaryLeg = burst.legs[0]!; + const corporateEventOffset = Number( + burst.flowFeatures.corporate_event_ts_offset_days ?? 0 + ); + const totalSize = burst.legs.reduce((sum, leg) => sum + leg.baseSize * burst.cycles, 0); + const totalPremium = burst.legs.reduce( + (sum, leg) => + sum + leg.basePrice * leg.baseSize * burst.cycles * OPTION_CONTRACT_MULTIPLIER, + 0 + ); const flowFeatures: FlowPacket["features"] = { - option_contract_id: burst.contractId, - underlying_id: burst.contractId.split("-")[0], + option_contract_id: primaryLeg.contractId, + underlying_id: primaryLeg.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)), + total_size: totalSize, + total_premium: Number(totalPremium.toFixed(2)), + total_notional: Number( + (burst.underlying * totalSize * OPTION_CONTRACT_MULTIPLIER).toFixed(2) + ), + first_price: primaryLeg.basePrice, + last_price: Number( + ( + primaryLeg.basePrice * + (1 + burst.priceStep * Math.max(0, burst.cycles - 1)) + ).toFixed(2) + ), nbbo_missing_count: 0, nbbo_stale_count: 0, ...burst.flowFeatures @@ -837,22 +1290,141 @@ export const buildSyntheticFlowPacketForTest = ( if (corporateEventOffset > 0) { flowFeatures.corporate_event_ts = now + corporateEventOffset * MS_PER_DAY; } + if (scenarioId === "retail_whale") { + const replacementStrike = Math.round((burst.underlying * 1.08) / 5) * 5; + flowFeatures.option_contract_id = `${primaryLeg.contractId.split("-")[0]}-${formatExpiry( + now, + 1 + )}-${formatStrike(replacementStrike)}-C`; + flowFeatures.total_premium = Math.min( + Number(flowFeatures.total_premium ?? totalPremium), + 72_000 + ); + flowFeatures.execution_iv_shock = Math.max( + Number(flowFeatures.execution_iv_shock ?? 0), + 0.22 + ); + } + if (scenarioId === "event_driven") { + flowFeatures.count = 2; + flowFeatures.window_ms = 45; + flowFeatures.total_size = 620; + flowFeatures.total_premium = 24_000; + flowFeatures.nbbo_coverage_ratio = 0.38; + flowFeatures.nbbo_aggressive_ratio = 0.32; + flowFeatures.nbbo_aggressive_buy_ratio = 0.3; + flowFeatures.nbbo_aggressive_sell_ratio = 0.08; + flowFeatures.nbbo_inside_ratio = 0.28; + flowFeatures.nbbo_spread_z = 0.18; + flowFeatures.venue_count = 2; + flowFeatures.corporate_event_ts = now + 7 * MS_PER_DAY; + } + if (scenarioId === "vol_seller") { + flowFeatures.same_size_leg_symmetry = 0.58; + flowFeatures.nbbo_aggressive_ratio = 0.74; + flowFeatures.nbbo_aggressive_buy_ratio = 0.06; + flowFeatures.nbbo_aggressive_sell_ratio = 0.72; + flowFeatures.nbbo_inside_ratio = 0.08; + } + if (scenarioId === "arbitrage") { + flowFeatures.count = 4; + flowFeatures.window_ms = 90; + flowFeatures.total_size = 1800; + flowFeatures.total_premium = 30_000; + flowFeatures.nbbo_coverage_ratio = 0.72; + flowFeatures.nbbo_aggressive_ratio = 0.3; + flowFeatures.nbbo_aggressive_buy_ratio = 0.3; + flowFeatures.nbbo_aggressive_sell_ratio = 0.26; + flowFeatures.nbbo_inside_ratio = 0.42; + flowFeatures.same_size_leg_symmetry = 0.94; + } + if (scenarioId === "hedge_reactive") { + const replacementStrike = Math.round(burst.underlying / 5) * 5; + flowFeatures.option_contract_id = `${primaryLeg.contractId.split("-")[0]}-${formatExpiry( + now, + 1 + )}-${formatStrike(replacementStrike)}-P`; + flowFeatures.count = 2; + flowFeatures.window_ms = 45; + flowFeatures.total_size = 1600; + flowFeatures.total_premium = 18_000; + flowFeatures.nbbo_coverage_ratio = 0.7; + flowFeatures.underlying_move_bps = -96; + flowFeatures.nbbo_aggressive_ratio = 0.32; + flowFeatures.nbbo_aggressive_buy_ratio = 0.3; + flowFeatures.nbbo_aggressive_sell_ratio = 0.08; + flowFeatures.nbbo_inside_ratio = 0.2; + } return { - hiddenLabel: burst.label, + hiddenLabel: burst.hiddenLabel, 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}`), + members: Array.from( + { length: burst.printCount }, + (_, index) => + `${burst.legs[index % burst.legs.length]?.contractId ?? primaryLeg.contractId}:${index + 1}` + ), features: flowFeatures, join_quality: {} } }; }; +export const buildSyntheticBurstForTest = ( + burstIndex: number, + now: number, + mode: SyntheticMarketMode +): Burst => { + const profile = SYNTHETIC_PROFILES[mode]; + const control: SyntheticControlState = { + preset_id: + mode === "realistic" ? "balanced_demo" : mode === "active" ? "balanced_demo" : "dealer_day", + coverage_assist: true, + coverage_window_minutes: 20, + shared_seed: 11, + profile_weights: { + institutional_directional: 1.0, + retail_whale: 1.0, + event_driven: 1.0, + vol_seller: 1.0, + arbitrage: 1.0, + hedge_reactive: 1.0 + }, + updated_at: 0, + updated_by: "system" + }; + const coverageState = createCoverageWindowState(); + const cacheKey = `${mode}:${now}`; + const cached = burstSequenceCache.get(cacheKey) ?? []; + if (!burstSequenceCache.has(cacheKey)) { + burstSequenceCache.set(cacheKey, cached); + } + for (let index = 0; index < cached.length; index += 1) { + recordCoverageHit(coverageState, cached[index]!.label, now + (index + 1) * 1_000); + } + if (cached.length >= burstIndex) { + return cached[burstIndex - 1]!; + } + for (let index = cached.length + 1; index <= burstIndex; index += 1) { + const current = buildBurst( + index, + now + index * 1_000, + mode, + profile, + control, + coverageState + ); + recordCoverageHit(coverageState, current.label, now + index * 1_000); + cached.push(current); + } + return cached[burstIndex - 1]!; +}; + export const createSyntheticOptionsAdapter = ( config: SyntheticOptionsAdapterConfig ): OptionIngestAdapter => { @@ -864,10 +1436,11 @@ export const createSyntheticOptionsAdapter = ( let nbboSeq = 0; let burstIndex = 0; let currentBurst: Burst | null = null; - const ivByContract = new Map(); let remainingRuns = 0; let timer: ReturnType | null = null; let stopped = false; + const ivByContract = new Map(); + const coverageState = createCoverageWindowState(); const emit = () => { if (stopped) { @@ -875,9 +1448,33 @@ export const createSyntheticOptionsAdapter = ( } const now = Date.now(); + const control = config.getControl?.() ?? { + preset_id: "balanced_demo", + coverage_assist: true, + coverage_window_minutes: 20, + shared_seed: 11, + profile_weights: { + institutional_directional: 1.0, + retail_whale: 1.0, + event_driven: 1.0, + vol_seller: 1.0, + arbitrage: 1.0, + hedge_reactive: 1.0 + }, + updated_at: 0, + updated_by: "system" + }; if (!currentBurst || remainingRuns <= 0) { burstIndex += 1; - currentBurst = buildBurst(burstIndex, now, profile); + currentBurst = buildBurst( + burstIndex, + now, + config.mode, + profile, + control, + coverageState + ); + recordCoverageHit(coverageState, currentBurst.label, now); remainingRuns = pickInt( profile.burstRunRange[0], profile.burstRunRange[1], @@ -886,82 +1483,109 @@ export const createSyntheticOptionsAdapter = ( } const burst = currentBurst; - const printsToEmit = burst.printCount; + const session = getSyntheticSessionState(now, control); + const underlyingState = getSyntheticUnderlyingState( + burst.contractId.split("-")[0]!, + now, + control, + session + ); - for (let i = 0; i < printsToEmit; i += 1) { - seq += 1; + for (let i = 0; i < burst.printCount; i += 1) { + const leg = burst.legs[i % burst.legs.length]!; + const legCycle = Math.floor(i / burst.legs.length); + const eventTs = now + i * 5; const priceJitter = ((i % 3) - 1) * 0.004; const sizeJitter = ((i % 3) - 1) * 0.08; - const priceMultiplier = 1 + burst.priceStep * i + priceJitter; - const placement = pickPlacement(burst, i, profile); - const size = Math.max(1, Math.round(burst.baseSize * (1 + sizeJitter))); - const previousIv = ivByContract.get(burst.contractId); - const provisionalNotional = burst.basePrice * size * OPTION_CONTRACT_MULTIPLIER; + const priceMultiplier = 1 + burst.priceStep * legCycle + priceJitter; + const placement = pickPlacement(burst, i); + const size = Math.max(1, Math.round(leg.baseSize * (1 + sizeJitter))); + const previousIv = ivByContract.get(leg.contractId); + const provisionalNotional = leg.basePrice * size * OPTION_CONTRACT_MULTIPLIER; const ivState = updateSyntheticIvForTest(previousIv, { - ts: now + i * 5, + ts: eventTs, placement, size, notional: provisionalNotional, - dteDays: burst.expiryOffsetDays, - moneyness: burst.strike / burst.underlying + dteDays: leg.expiryOffsetDays, + moneyness: leg.strike / burst.underlying }); - ivByContract.set(burst.contractId, ivState); - const ivDrift = Math.max(0, ivState.iv - initializeSyntheticIv(burst.expiryOffsetDays, burst.strike / burst.underlying)); + ivByContract.set(leg.contractId, ivState); + const ivDrift = Math.max( + 0, + ivState.iv - initializeSyntheticIv(leg.expiryOffsetDays, leg.strike / burst.underlying) + ); const mid = Math.max( 0.05, - Number((burst.basePrice * priceMultiplier * (1 + ivDrift * 1.15)).toFixed(2)) + Number((leg.basePrice * priceMultiplier * (1 + ivDrift * 1.15)).toFixed(2)) + ); + const spread = Math.max( + 0.02, + Number( + ( + mid * + (0.018 + + Math.min(0.04, ivState.iv * 0.01) + + underlyingState.sessionVolatility * 0.01 + + (1 - underlyingState.quoteCleanliness) * 0.006) + ).toFixed(2) + ) ); - const spread = Math.max(0.02, Number((mid * (0.02 + Math.min(0.035, ivState.iv * 0.01))).toFixed(2))); const bid = Math.max(0.01, Number((mid - spread / 2).toFixed(2))); const ask = Math.max(bid + 0.01, Number((mid + spread / 2).toFixed(2))); const tick = Math.max(0.01, Number((spread * 0.25).toFixed(2))); - let tradePrice = mid; - - if (placement === "AA") { - tradePrice = ask + tick; - } else if (placement === "A") { - tradePrice = ask; - } else if (placement === "MID") { - tradePrice = mid; - } else if (placement === "BB") { - tradePrice = Math.max(0.01, bid - tick); - } else { - tradePrice = bid; - } + const tradePrice = + placement === "AA" + ? ask + tick + : placement === "A" + ? ask + : placement === "BB" + ? Math.max(0.01, bid - tick) + : placement === "B" + ? bid + : mid; + seq += 1; const print: OptionPrint = { - source_ts: now + i * 5, - ingest_ts: now + i * 5, + source_ts: eventTs, + ingest_ts: eventTs, seq, trace_id: `synthetic-options-${seq}`, - ts: now + i * 5, - option_contract_id: burst.contractId, + ts: eventTs, + option_contract_id: leg.contractId, price: tradePrice, size, - exchange: burst.exchange, + exchange: leg.exchange, conditions: burst.conditions, execution_iv: ivState.iv, - execution_iv_source: "synthetic_pressure_model" + execution_iv_source: "synthetic_pressure_model", + execution_underlying_mid: burst.underlying }; - if (handlers.onNBBO) { + const quoteSeed = Math.abs(burst.seed + i * 17) % 1000; + const missingQuote = quoteSeed / 1000 < burst.missingQuoteProbability; + const staleQuote = + !missingQuote && + ((quoteSeed + 233) % 1000) / 1000 < burst.staleQuoteProbability; + + if (handlers.onNBBO && !missingQuote) { nbboSeq += 1; - const sizeBase = Math.max(1, Math.round(burst.baseSize * 0.4)); + const sizeBase = Math.max(1, Math.round(leg.baseSize * 0.4)); const bidSize = Math.max(1, Math.round(sizeBase * (1 + sizeJitter))); const askSize = Math.max(1, Math.round(sizeBase * (1 - sizeJitter))); + const quoteTs = staleQuote ? eventTs - 2_000 : eventTs; const nbbo: OptionNBBO = { - source_ts: print.ts, - ingest_ts: print.ingest_ts, + source_ts: quoteTs, + ingest_ts: quoteTs, seq: nbboSeq, trace_id: `synthetic-nbbo-${nbboSeq}`, - ts: print.ts, - option_contract_id: burst.contractId, + ts: quoteTs, + option_contract_id: leg.contractId, bid, ask, bidSize, askSize }; - void handlers.onNBBO(nbbo); } diff --git a/services/ingest-options/src/index.ts b/services/ingest-options/src/index.ts index 84d7bfe..a52661f 100644 --- a/services/ingest-options/src/index.ts +++ b/services/ingest-options/src/index.ts @@ -11,9 +11,12 @@ import { STREAM_OPTION_SIGNAL_PRINTS, buildDurableConsumer, connectJetStreamWithRetry, + ensureSyntheticControlState, ensureKnownStreams, + openSyntheticControlKv, publishJson, - subscribeJson + subscribeJson, + watchSyntheticControlState } from "@islandflow/bus"; import { createClickHouseClient, @@ -26,12 +29,14 @@ import { OptionNBBOSchema, OptionPrintSchema, EquityQuoteSchema, + DEFAULT_SYNTHETIC_CONTROL_STATE, deriveOptionPrintMetadata, resolveSyntheticMarketModes, type EquityQuote, type OptionNBBO, type OptionPrint, - type OptionsSignalConfig + type OptionsSignalConfig, + type SyntheticControlState } from "@islandflow/types"; import { createAlpacaOptionsAdapter } from "./adapters/alpaca"; import { createDatabentoOptionsAdapter } from "./adapters/databento"; @@ -259,11 +264,15 @@ const retry = async ( throw lastError ?? new Error(`${label} failed after retries`); }; -const selectAdapter = (name: string): OptionIngestAdapter => { +const selectAdapter = ( + name: string, + getSyntheticControl: () => SyntheticControlState +): OptionIngestAdapter => { if (name === "synthetic") { return createSyntheticOptionsAdapter({ emitIntervalMs: env.EMIT_INTERVAL_MS, - mode: syntheticModes.options + mode: syntheticModes.options, + getControl: getSyntheticControl }); } @@ -351,6 +360,24 @@ const run = async () => { { logger } ); + let syntheticControl = DEFAULT_SYNTHETIC_CONTROL_STATE; + let stopSyntheticControlWatch = async () => {}; + if (env.OPTIONS_INGEST_ADAPTER === "synthetic") { + const syntheticControlKv = await openSyntheticControlKv(js); + syntheticControl = await ensureSyntheticControlState(syntheticControlKv); + stopSyntheticControlWatch = await watchSyntheticControlState( + syntheticControlKv, + (nextControl) => { + syntheticControl = nextControl; + }, + (error) => { + logger.warn("synthetic control watch failed", { + error: getErrorMessage(error) + }); + } + ); + } + const clickhouse = createClickHouseClient({ url: env.CLICKHOUSE_URL, database: env.CLICKHOUSE_DATABASE @@ -361,7 +388,10 @@ const run = async () => { await ensureOptionNBBOTable(clickhouse); }); - const adapter = selectAdapter(env.OPTIONS_INGEST_ADAPTER); + const adapter = selectAdapter( + env.OPTIONS_INGEST_ADAPTER, + () => syntheticControl + ); logger.info("ingest adapter selected", { adapter: adapter.name }); const allowPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS); const allowNbboPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS); @@ -482,6 +512,7 @@ const run = async () => { state.shutdownPromise = (async () => { logger.info("service stopping", { signal }); clearInterval(pruneTimer); + await stopSyntheticControlWatch(); await stopAdapter(); try { diff --git a/services/ingest-options/tests/synthetic.test.ts b/services/ingest-options/tests/synthetic.test.ts index 6db43a3..fd299a9 100644 --- a/services/ingest-options/tests/synthetic.test.ts +++ b/services/ingest-options/tests/synthetic.test.ts @@ -10,26 +10,43 @@ import { } from "../src/adapters/synthetic"; const totalBurstNotional = (burst: { - basePrice: number; - baseSize: number; - printCount: number; -}): number => burst.basePrice * burst.baseSize * burst.printCount * 100; + legs: Array<{ + basePrice: number; + baseSize: number; + }>; + cycles: number; +}): number => + burst.legs.reduce((sum, leg) => sum + leg.basePrice * leg.baseSize * burst.cycles * 100, 0); + +const findBurst = ( + mode: "realistic" | "active", + scenarioId: string, + now = Date.UTC(2026, 0, 2) +) => { + for (let i = 1; i <= 360; i += 1) { + const burst = buildSyntheticBurstForTest(i, now + i * 1_000, mode); + if (burst.scenarioId === scenarioId) { + return burst; + } + } + throw new Error(`Unable to find synthetic scenario ${scenarioId} in mode ${mode}`); +}; describe("synthetic options burst sizing", () => { - it("keeps realistic-mode ask lifts inside the configured notional band", () => { - const burst = buildSyntheticBurstForTest(2, Date.UTC(2026, 0, 2), "realistic"); + it("keeps realistic-mode ask-lift accumulation inside the configured notional band", () => { + const burst = findBurst("realistic", "ask_lift_accumulation"); - expect(burst.scenarioId).toBe("ask_lift"); - expect(totalBurstNotional(burst)).toBeGreaterThanOrEqual(9_000); - expect(totalBurstNotional(burst)).toBeLessThanOrEqual(35_000); + expect(burst.scenarioId).toBe("ask_lift_accumulation"); + expect(totalBurstNotional(burst)).toBeGreaterThanOrEqual(12_000); + expect(totalBurstNotional(burst)).toBeLessThanOrEqual(90_000); }); - it("keeps active-mode sweeps inside the configured notional band", () => { - const burst = buildSyntheticBurstForTest(1, Date.UTC(2026, 0, 2), "active"); + it("keeps active-mode call sweeps inside the configured notional band", () => { + const burst = findBurst("active", "call_sweep"); - expect(burst.scenarioId).toBe("bearish_sweep"); - expect(totalBurstNotional(burst)).toBeGreaterThanOrEqual(120_000); - expect(totalBurstNotional(burst)).toBeLessThanOrEqual(240_000); + expect(burst.scenarioId).toBe("call_sweep"); + expect(totalBurstNotional(burst)).toBeGreaterThanOrEqual(70_000); + expect(totalBurstNotional(burst)).toBeLessThanOrEqual(420_000); }); }); @@ -114,7 +131,7 @@ describe("synthetic smart-money scenarios", () => { 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" + (scenario) => scenario.label !== "neutral_noise" ); for (const scenario of scenarios) { @@ -122,17 +139,62 @@ describe("synthetic smart-money scenarios", () => { 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 + (score) => score.profile_id !== scenario.label && 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(event.primary_profile_id, scenario.id).toBe(scenario.label); + expect(winningScore?.profile_id, scenario.id).toBe(scenario.label); expect(winningScore?.probability ?? 0, scenario.id).toBeGreaterThanOrEqual(0.5); + expect(hiddenLabel.length, scenario.id).toBeGreaterThan(0); expect(nearbyWrongScores, scenario.id).toEqual([]); } }); + it("covers every smart-money label in active runtime mode over a deterministic sample", () => { + const seen = new Set(); + const now = Date.parse("2026-01-02T15:00:00Z"); + + for (let i = 1; i <= 120; i += 1) { + const burst = buildSyntheticBurstForTest(i, now + i * 1_000, "active"); + seen.add(burst.label); + } + + expect(seen).toEqual( + new Set([ + "institutional_directional", + "retail_whale", + "event_driven", + "vol_seller", + "arbitrage", + "hedge_reactive", + "neutral_noise" + ]) + ); + }); + + it("covers every smart-money label in realistic mode within a default twenty-minute window", () => { + const seen = new Set(); + const now = Date.parse("2026-01-02T15:00:00Z"); + + for (let i = 1; i <= 120; i += 1) { + const burst = buildSyntheticBurstForTest(i, now + i * 10_000, "realistic"); + seen.add(burst.label); + } + + expect(seen).toEqual( + new Set([ + "institutional_directional", + "retail_whale", + "event_driven", + "vol_seller", + "arbitrage", + "hedge_reactive", + "neutral_noise" + ]) + ); + }); + it("keeps neutral background noise below the emission threshold", () => { const { packet } = buildSyntheticFlowPacketForTest( "neutral_noise",