Add hosted synthetic control plane

This commit is contained in:
dirtydishes 2026-05-13 22:10:05 -04:00
parent af04875107
commit 8dcbcd2201
21 changed files with 3695 additions and 772 deletions

View file

@ -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<typeof getSyntheticSessionState>["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<typeof getSyntheticSessionState>["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<typeof setInterval> | 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);

View file

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