Add hosted synthetic control plane
This commit is contained in:
parent
af04875107
commit
8dcbcd2201
21 changed files with 3695 additions and 772 deletions
|
|
@ -6,6 +6,7 @@
|
|||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@islandflow/types": "workspace:*",
|
||||
"nats": "^2.24.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./jetstream";
|
||||
export * from "./streams";
|
||||
export * from "./subjects";
|
||||
export * from "./synthetic-control";
|
||||
|
|
|
|||
100
packages/bus/src/synthetic-control.ts
Normal file
100
packages/bus/src/synthetic-control.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import {
|
||||
DEFAULT_SYNTHETIC_CONTROL_STATE,
|
||||
SyntheticControlStateSchema,
|
||||
normalizeSyntheticControlState,
|
||||
type SyntheticControlState
|
||||
} from "@islandflow/types";
|
||||
import { JSONCodec, type JetStreamClient, type KV, type KvEntry } from "nats";
|
||||
|
||||
export const SYNTHETIC_CONTROL_BUCKET = "synthetic_control";
|
||||
export const SYNTHETIC_CONTROL_GLOBAL_KEY = "global";
|
||||
|
||||
const codec = JSONCodec<SyntheticControlState>();
|
||||
|
||||
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<KV> => {
|
||||
return js.views.kv(SYNTHETIC_CONTROL_BUCKET, {
|
||||
description: "Hosted synthetic market internal control state",
|
||||
history: 8
|
||||
});
|
||||
};
|
||||
|
||||
export const readSyntheticControlState = async (
|
||||
kv: KV
|
||||
): Promise<SyntheticControlState> => {
|
||||
return decodeSyntheticControlEntry(
|
||||
await kv.get(SYNTHETIC_CONTROL_GLOBAL_KEY)
|
||||
);
|
||||
};
|
||||
|
||||
export const ensureSyntheticControlState = async (
|
||||
kv: KV
|
||||
): Promise<SyntheticControlState> => {
|
||||
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<SyntheticControlState>
|
||||
): Promise<SyntheticControlState> => {
|
||||
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<void>> => {
|
||||
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;
|
||||
};
|
||||
};
|
||||
834
packages/types/src/synthetic-market.ts
Normal file
834
packages/types/src/synthetic-market.ts
Normal file
|
|
@ -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<typeof SyntheticControlPresetIdSchema>;
|
||||
|
||||
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<typeof SyntheticControlStateSchema>;
|
||||
|
||||
export const SyntheticSessionPhaseSchema = z.enum([
|
||||
"open",
|
||||
"midday",
|
||||
"power_hour",
|
||||
"after_event"
|
||||
]);
|
||||
export type SyntheticSessionPhase = z.infer<typeof SyntheticSessionPhaseSchema>;
|
||||
|
||||
export const SyntheticRegimeSchema = z.enum(REGIME_IDS);
|
||||
export type SyntheticRegime = z.infer<typeof SyntheticRegimeSchema>;
|
||||
|
||||
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<SmartMoneyProfileId, number>;
|
||||
};
|
||||
|
||||
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<SyntheticRegime, number>
|
||||
> = {
|
||||
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 = <T,>(items: readonly T[], seed: number): T => {
|
||||
const index = Math.abs(seed) % items.length;
|
||||
return items[index]!;
|
||||
};
|
||||
|
||||
const pickManyUnique = <T,>(
|
||||
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 = <T extends string>(
|
||||
weights: Record<T, number>,
|
||||
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<SyntheticControlState> | 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<SyntheticControlState> | 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<SyntheticRegime, number>
|
||||
);
|
||||
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<SyntheticControlState> | SyntheticMarketMode | null | undefined
|
||||
): value is SyntheticMarketMode => {
|
||||
return value === "realistic" || value === "active" || value === "firehose";
|
||||
};
|
||||
|
||||
export const getSyntheticUnderlyingState = (
|
||||
symbol: string,
|
||||
ts: number,
|
||||
controlOrMode:
|
||||
| Partial<SyntheticControlState>
|
||||
| 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<SyntheticControlState> | 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<SyntheticControlState>
|
||||
| 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;
|
||||
104
packages/types/tests/synthetic-market.test.ts
Normal file
104
packages/types/tests/synthetic-market.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue