Implement smart money event bridge

This commit is contained in:
dirtydishes 2026-05-04 17:36:03 -04:00
parent a8cc2e3875
commit 6822fa1ba4
16 changed files with 1047 additions and 15 deletions

View file

@ -9,6 +9,7 @@ import {
SUBJECT_EQUITY_QUOTES,
SUBJECT_INFERRED_DARK,
SUBJECT_FLOW_PACKETS,
SUBJECT_SMART_MONEY_EVENTS,
SUBJECT_OPTION_NBBO,
SUBJECT_OPTION_SIGNAL_PRINTS,
STREAM_ALERTS,
@ -19,6 +20,7 @@ import {
STREAM_EQUITY_QUOTES,
STREAM_INFERRED_DARK,
STREAM_FLOW_PACKETS,
STREAM_SMART_MONEY_EVENTS,
STREAM_OPTION_NBBO,
STREAM_OPTION_SIGNAL_PRINTS,
buildDurableConsumer,
@ -36,17 +38,21 @@ import {
ensureEquityQuotesTable,
ensureInferredDarkTable,
ensureFlowPacketsTable,
ensureSmartMoneyEventsTable,
ensureOptionNBBOTable,
ensureOptionPrintsTable,
fetchAlertsAfter,
fetchAlertsBefore,
fetchClassifierHitsAfter,
fetchClassifierHitsBefore,
fetchSmartMoneyEventsAfter,
fetchSmartMoneyEventsBefore,
fetchFlowPacketsAfter,
fetchFlowPacketById,
fetchFlowPacketsBefore,
fetchRecentAlerts,
fetchRecentClassifierHits,
fetchRecentSmartMoneyEvents,
fetchRecentEquityPrintJoins,
fetchRecentFlowPackets,
fetchRecentInferredDark,
@ -95,6 +101,7 @@ import {
OptionSecurityTypeSchema,
OptionTypeSchema,
FlowPacketSchema,
SmartMoneyEventSchema,
OptionNBBOSchema,
OptionPrintSchema,
getSubscriptionKey
@ -256,6 +263,7 @@ type Channel =
| "equity-joins"
| "inferred-dark"
| "flow"
| "smart-money"
| "classifier-hits"
| "alerts";
@ -278,6 +286,7 @@ const equityQuoteSockets = new Set<LegacySocket>();
const equityJoinSockets = new Set<LegacySocket>();
const inferredDarkSockets = new Set<LegacySocket>();
const flowSockets = new Set<LegacySocket>();
const smartMoneySockets = new Set<LegacySocket>();
const classifierHitSockets = new Set<LegacySocket>();
const alertSockets = new Set<LegacySocket>();
const liveSocketSubscriptions = new Map<LiveSocket, Set<string>>();
@ -772,6 +781,19 @@ const run = async () => {
num_replicas: 1
});
await ensureStream(jsm, {
name: STREAM_SMART_MONEY_EVENTS,
subjects: [SUBJECT_SMART_MONEY_EVENTS],
retention: "limits",
storage: "file",
discard: "old",
max_msgs_per_subject: -1,
max_msgs: -1,
max_bytes: -1,
max_age: 0,
num_replicas: 1
});
await ensureStream(jsm, {
name: STREAM_CLASSIFIER_HITS,
subjects: [SUBJECT_CLASSIFIER_HITS],
@ -812,6 +834,7 @@ const run = async () => {
await ensureEquityPrintJoinsTable(clickhouse);
await ensureInferredDarkTable(clickhouse);
await ensureFlowPacketsTable(clickhouse);
await ensureSmartMoneyEventsTable(clickhouse);
await ensureClassifierHitsTable(clickhouse);
await ensureAlertsTable(clickhouse);
});
@ -918,6 +941,11 @@ const run = async () => {
stream: STREAM_FLOW_PACKETS,
durableName: "api-flow-packets"
},
{
subject: SUBJECT_SMART_MONEY_EVENTS,
stream: STREAM_SMART_MONEY_EVENTS,
durableName: "api-smart-money-events"
},
{
subject: SUBJECT_CLASSIFIER_HITS,
stream: STREAM_CLASSIFIER_HITS,
@ -1057,18 +1085,24 @@ const run = async () => {
consumerBindings[7].durableName
);
const classifierHitSubscription = await subscribeWithReset(
const smartMoneySubscription = await subscribeWithReset(
consumerBindings[8].subject,
consumerBindings[8].stream,
consumerBindings[8].durableName
);
const alertSubscription = await subscribeWithReset(
const classifierHitSubscription = await subscribeWithReset(
consumerBindings[9].subject,
consumerBindings[9].stream,
consumerBindings[9].durableName
);
const alertSubscription = await subscribeWithReset(
consumerBindings[10].subject,
consumerBindings[10].stream,
consumerBindings[10].durableName
);
const fanoutLive = async (
subscription: LiveSubscription,
item: unknown,
@ -1269,6 +1303,22 @@ const run = async () => {
}
};
const pumpSmartMoney = async () => {
for await (const msg of smartMoneySubscription.messages) {
try {
const payload = SmartMoneyEventSchema.parse(smartMoneySubscription.decode(msg));
broadcast(smartMoneySockets, { type: "smart-money", payload });
await fanoutLive({ channel: "smart-money" }, payload, "smart-money");
msg.ack();
} catch (error) {
logger.error("failed to process smart money event", {
error: error instanceof Error ? error.message : String(error)
});
msg.term();
}
}
};
const pumpClassifierHits = async () => {
for await (const msg of classifierHitSubscription.messages) {
try {
@ -1309,6 +1359,7 @@ const run = async () => {
void pumpEquityJoins();
void pumpInferredDark();
void pumpFlow();
void pumpSmartMoney();
void pumpClassifierHits();
void pumpAlerts();
@ -1429,6 +1480,12 @@ const run = async () => {
return jsonResponse({ data });
}
if (req.method === "GET" && url.pathname === "/flow/smart-money") {
const limit = parseLimit(url.searchParams.get("limit"));
const data = await fetchRecentSmartMoneyEvents(clickhouse, limit);
return jsonResponse({ data });
}
if (req.method === "GET" && url.pathname === "/flow/classifier-hits") {
const limit = parseLimit(url.searchParams.get("limit"));
const data = await fetchRecentClassifierHits(clickhouse, limit);
@ -1507,6 +1564,14 @@ const run = async () => {
);
}
if (req.method === "GET" && url.pathname === "/history/smart-money") {
const { beforeTs, beforeSeq, limit } = parseBeforeParams(url);
const data = await fetchSmartMoneyEventsBefore(clickhouse, beforeTs, beforeSeq, limit);
return jsonResponse(
buildHistoryResponse(data, (item) => ({ ts: item.source_ts, seq: item.seq }))
);
}
if (req.method === "GET" && url.pathname === "/history/classifier-hits") {
const { beforeTs, beforeSeq, limit } = parseBeforeParams(url);
const data = await fetchClassifierHitsBefore(clickhouse, beforeTs, beforeSeq, limit);
@ -1651,6 +1716,14 @@ const run = async () => {
return jsonResponse({ data, next });
}
if (req.method === "GET" && url.pathname === "/replay/smart-money") {
const { afterTs, afterSeq, limit } = parseReplayParams(url);
const data = await fetchSmartMoneyEventsAfter(clickhouse, afterTs, afterSeq, limit);
const last = data.at(-1);
const next = last ? { ts: last.source_ts, seq: last.seq } : null;
return jsonResponse({ data, next });
}
if (req.method === "GET" && url.pathname === "/replay/classifier-hits") {
const { afterTs, afterSeq, limit } = parseReplayParams(url);
const data = await fetchClassifierHitsAfter(clickhouse, afterTs, afterSeq, limit);
@ -1739,6 +1812,14 @@ const run = async () => {
return jsonResponse({ error: "websocket upgrade failed" }, 400);
}
if (req.method === "GET" && url.pathname === "/ws/smart-money") {
if (serverRef.upgrade(req, { data: { channel: "smart-money" } })) {
return new Response(null, { status: 101 });
}
return jsonResponse({ error: "websocket upgrade failed" }, 400);
}
if (req.method === "GET" && url.pathname === "/ws/alerts") {
if (serverRef.upgrade(req, { data: { channel: "alerts" } })) {
return new Response(null, { status: 101 });
@ -1781,6 +1862,8 @@ const run = async () => {
inferredDarkSockets.add(socket);
} else if (socket.data.channel === "flow") {
flowSockets.add(socket);
} else if (socket.data.channel === "smart-money") {
smartMoneySockets.add(socket);
} else if (socket.data.channel === "classifier-hits") {
classifierHitSockets.add(socket);
} else {
@ -1842,6 +1925,8 @@ const run = async () => {
inferredDarkSockets.delete(socket);
} else if (socket.data.channel === "flow") {
flowSockets.delete(socket);
} else if (socket.data.channel === "smart-money") {
smartMoneySockets.delete(socket);
} else if (socket.data.channel === "classifier-hits") {
classifierHitSockets.delete(socket);
} else {

View file

@ -9,6 +9,7 @@ import {
fetchRecentFlowPackets,
fetchRecentInferredDark,
fetchRecentOptionNBBO,
fetchRecentSmartMoneyEvents,
type ClickHouseClient
} from "@islandflow/storage";
import type { OptionPrintQueryFilters } from "@islandflow/storage";
@ -30,6 +31,7 @@ import {
matchesOptionPrintFilters,
OptionNBBOSchema,
OptionPrintSchema,
SmartMoneyEventSchema,
type OptionFlowFilters,
type Cursor,
type EquityCandle,
@ -51,6 +53,7 @@ const GENERIC_LIMIT_ENV_KEYS: Record<LiveGenericChannel, string> = {
"equity-quotes": "LIVE_LIMIT_EQUITY_QUOTES",
"equity-joins": "LIVE_LIMIT_EQUITY_JOINS",
flow: "LIVE_LIMIT_FLOW",
"smart-money": "LIVE_LIMIT_SMART_MONEY",
"classifier-hits": "LIVE_LIMIT_CLASSIFIER_HITS",
alerts: "LIVE_LIMIT_ALERTS",
"inferred-dark": "LIVE_LIMIT_INFERRED_DARK"
@ -111,6 +114,7 @@ export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env):
"equity-quotes": parseGenericLimit(env, "equity-quotes", DEFAULT_GENERIC_LIMIT),
"equity-joins": parseGenericLimit(env, "equity-joins", DEFAULT_GENERIC_LIMIT),
flow: parseGenericLimit(env, "flow", DEFAULT_GENERIC_LIMIT),
"smart-money": parseGenericLimit(env, "smart-money", DEFAULT_GENERIC_LIMIT),
"classifier-hits": parseGenericLimit(env, "classifier-hits", DEFAULT_GENERIC_LIMIT),
alerts: parseGenericLimit(env, "alerts", DEFAULT_GENERIC_LIMIT),
"inferred-dark": parseGenericLimit(env, "inferred-dark", DEFAULT_GENERIC_LIMIT)
@ -185,6 +189,14 @@ const getGenericConfig = (limits: GenericLiveLimits): {
cursor: (item) => ({ ts: item.source_ts, seq: item.seq }),
fetchRecent: fetchRecentFlowPackets
},
"smart-money": {
redisKey: "live:smart-money",
cursorField: "smart-money",
limit: limits["smart-money"],
parse: (value) => SmartMoneyEventSchema.parse(value),
cursor: (item) => ({ ts: item.source_ts, seq: item.seq }),
fetchRecent: fetchRecentSmartMoneyEvents
},
"classifier-hits": {
redisKey: "live:classifier-hits",
cursorField: "classifier-hits",

View file

@ -154,6 +154,7 @@ describe("LiveStateManager", () => {
"equity-quotes": 10000,
"equity-joins": 10000,
flow: 2,
"smart-money": 10000,
"classifier-hits": 10000,
alerts: 10000,
"inferred-dark": 10000

View file

@ -8,6 +8,7 @@ import {
SUBJECT_EQUITY_QUOTES,
SUBJECT_INFERRED_DARK,
SUBJECT_FLOW_PACKETS,
SUBJECT_SMART_MONEY_EVENTS,
SUBJECT_OPTION_NBBO,
SUBJECT_OPTION_SIGNAL_PRINTS,
STREAM_ALERTS,
@ -17,6 +18,7 @@ import {
STREAM_EQUITY_QUOTES,
STREAM_INFERRED_DARK,
STREAM_FLOW_PACKETS,
STREAM_SMART_MONEY_EVENTS,
STREAM_OPTION_NBBO,
STREAM_OPTION_SIGNAL_PRINTS,
buildDurableConsumer,
@ -32,11 +34,13 @@ import {
ensureEquityPrintJoinsTable,
ensureInferredDarkTable,
ensureFlowPacketsTable,
ensureSmartMoneyEventsTable,
insertAlert,
insertClassifierHit,
insertEquityPrintJoin,
insertInferredDark,
insertFlowPacket
insertFlowPacket,
insertSmartMoneyEvent
} from "@islandflow/storage";
import {
AlertEventSchema,
@ -46,6 +50,7 @@ import {
EquityQuoteSchema,
InferredDarkEventSchema,
FlowPacketSchema,
SmartMoneyEventSchema,
OptionNBBOSchema,
OptionPrintSchema,
type AlertEvent,
@ -55,11 +60,16 @@ import {
type EquityPrintJoin,
type InferredDarkEvent,
type FlowPacket,
type SmartMoneyEvent,
type OptionNBBO,
type OptionPrint
} from "@islandflow/types";
import { z } from "zod";
import { evaluateClassifiers, type ClassifierConfig } from "./classifiers";
import type { ClassifierConfig } from "./classifiers";
import {
buildSmartMoneyEventFromPacket,
deriveClassifierHitsFromSmartMoneyEvent
} from "./parent-events";
import { parseContractId } from "./contracts";
import {
createDarkInferenceState,
@ -886,7 +896,23 @@ const emitClassifiers = async (
js: Awaited<ReturnType<typeof connectJetStreamWithRetry>>["js"],
packet: FlowPacket
): Promise<void> => {
const hits = evaluateClassifiers(packet, classifierConfig);
let smartMoneyEvent: SmartMoneyEvent;
try {
smartMoneyEvent = SmartMoneyEventSchema.parse(buildSmartMoneyEventFromPacket(packet));
await insertSmartMoneyEvent(clickhouse, smartMoneyEvent);
await publishJson(js, SUBJECT_SMART_MONEY_EVENTS, smartMoneyEvent);
} catch (error) {
if (isExpectedShutdownNatsError(error)) {
return;
}
logger.error("failed to emit smart money event", {
error: error instanceof Error ? error.message : String(error),
packet_id: packet.id
});
return;
}
const hits = deriveClassifierHitsFromSmartMoneyEvent(smartMoneyEvent);
if (hits.length === 0) {
return;
}
@ -922,7 +948,7 @@ const emitClassifiers = async (
source_ts: packet.source_ts,
ingest_ts: packet.ingest_ts,
seq: packet.seq,
trace_id: `alert:${packet.id}`,
trace_id: `alert:${smartMoneyEvent.event_id}`,
score,
severity,
hits: hitEvents.map((hit) => ({
@ -931,7 +957,11 @@ const emitClassifiers = async (
direction: hit.direction,
explanations: hit.explanations
})),
evidence_refs: [packet.id, ...packet.members]
evidence_refs: [smartMoneyEvent.event_id, packet.id, ...packet.members],
...(smartMoneyEvent.primary_profile_id
? { primary_profile_id: smartMoneyEvent.primary_profile_id }
: {}),
profile_scores: smartMoneyEvent.profile_scores
});
try {
@ -1100,6 +1130,19 @@ const run = async () => {
num_replicas: 1
});
await ensureStream(jsm, {
name: STREAM_SMART_MONEY_EVENTS,
subjects: [SUBJECT_SMART_MONEY_EVENTS],
retention: "limits",
storage: "file",
discard: "old",
max_msgs_per_subject: -1,
max_msgs: -1,
max_bytes: -1,
max_age: 0,
num_replicas: 1
});
await ensureStream(jsm, {
name: STREAM_EQUITY_JOINS,
subjects: [SUBJECT_EQUITY_JOINS],
@ -1173,6 +1216,7 @@ const run = async () => {
await retry("clickhouse table init", 120, 500, async () => {
await ensureFlowPacketsTable(clickhouse);
await ensureSmartMoneyEventsTable(clickhouse);
await ensureEquityPrintJoinsTable(clickhouse);
await ensureInferredDarkTable(clickhouse);
await ensureClassifierHitsTable(clickhouse);

View file

@ -0,0 +1,320 @@
import {
SmartMoneyEventSchema,
type ClassifierHit,
type FlowPacket,
type SmartMoneyDirection,
type SmartMoneyEvent,
type SmartMoneyFeatures,
type SmartMoneyProfileId,
type SmartMoneyProfileScore
} from "@islandflow/types";
import { parseContractId } from "./contracts";
const MS_PER_DAY = 86_400_000;
const SPECIAL_CONDITIONS = new Set(["AUCTION", "CROSS", "OPENING", "CLOSING", "COMPLEX", "SPREAD"]);
const clamp = (value: number, min = 0, max = 1): number => {
if (!Number.isFinite(value)) {
return min;
}
return Math.max(min, Math.min(max, value));
};
const numberFeature = (packet: FlowPacket, key: string): number => {
const value = packet.features[key];
return typeof value === "number" && Number.isFinite(value) ? value : 0;
};
const stringFeature = (packet: FlowPacket, key: string): string => {
const value = packet.features[key];
return typeof value === "string" ? value : "";
};
const boolFeature = (packet: FlowPacket, key: string): boolean | null => {
const value = packet.features[key];
return typeof value === "boolean" ? value : null;
};
const confidenceBand = (probability: number): SmartMoneyProfileScore["confidence_band"] => {
if (probability >= 0.72) {
return "high";
}
if (probability >= 0.52) {
return "medium";
}
return "low";
};
const score = (
profile_id: SmartMoneyProfileId,
probability: number,
direction: SmartMoneyDirection,
reasons: string[]
): SmartMoneyProfileScore => ({
profile_id,
probability: clamp(probability),
confidence_band: confidenceBand(probability),
direction,
reasons
});
const getReferenceTs = (packet: FlowPacket): number => {
return numberFeature(packet, "end_ts") || packet.source_ts;
};
const getDteDays = (packet: FlowPacket): number | null => {
const contract = parseContractId(stringFeature(packet, "option_contract_id"));
if (!contract) {
return null;
}
const expiryTs = Date.parse(`${contract.expiry}T00:00:00Z`);
if (!Number.isFinite(expiryTs)) {
return null;
}
const diff = expiryTs - getReferenceTs(packet);
return diff >= 0 ? Math.ceil(diff / MS_PER_DAY) : null;
};
const inferDirection = (packet: FlowPacket): SmartMoneyDirection => {
const structureRights = stringFeature(packet, "structure_rights");
const optionType = stringFeature(packet, "option_type") || parseContractId(stringFeature(packet, "option_contract_id"))?.right;
const buy = numberFeature(packet, "nbbo_aggressive_buy_ratio");
const sell = numberFeature(packet, "nbbo_aggressive_sell_ratio");
const sellDominant = sell >= buy + 0.12;
if (structureRights === "C") {
return sellDominant ? "bearish" : "bullish";
}
if (structureRights === "P") {
return sellDominant ? "bullish" : "bearish";
}
if (optionType === "C") {
return sellDominant ? "bearish" : "bullish";
}
if (optionType === "P") {
return sellDominant ? "bullish" : "bearish";
}
return "neutral";
};
const buildFeatures = (packet: FlowPacket): SmartMoneyFeatures => {
const contractId = stringFeature(packet, "option_contract_id");
const contract = parseContractId(contractId);
const underlyingMid = numberFeature(packet, "underlying_mid");
const quoteAge = numberFeature(packet, "nbbo_age_ms") || numberFeature(packet, "underlying_quote_age_ms");
const printCount = Math.max(0, Math.round(numberFeature(packet, "count") || packet.members.length));
const staleCount = numberFeature(packet, "nbbo_stale_count");
const missingCount = numberFeature(packet, "nbbo_missing_count");
const structureLegs = Math.max(0, Math.round(numberFeature(packet, "structure_legs")));
const strikeCount = Math.max(1, Math.round(numberFeature(packet, "structure_strikes") || (contract ? 1 : 0)));
const specialCount = numberFeature(packet, "special_print_count");
const eventTs = numberFeature(packet, "corporate_event_ts");
const referenceTs = getReferenceTs(packet);
const expiryTs = contract ? Date.parse(`${contract.expiry}T00:00:00Z`) : Number.NaN;
const atmProximity =
contract && underlyingMid > 0 ? Math.abs(contract.strike - underlyingMid) / underlyingMid : null;
return {
contract_count: Math.max(1, structureLegs || 1),
print_count: printCount,
total_size: numberFeature(packet, "total_size"),
total_premium: numberFeature(packet, "total_premium"),
total_notional: numberFeature(packet, "total_notional"),
start_ts: numberFeature(packet, "start_ts") || packet.source_ts,
end_ts: numberFeature(packet, "end_ts") || packet.source_ts,
window_ms: Math.max(0, Math.round(numberFeature(packet, "window_ms"))),
...(contractId ? { option_contract_id: contractId } : {}),
...(contract?.right === "C" || contract?.right === "P" ? { option_type: contract.right } : {}),
dte_days: getDteDays(packet),
moneyness: contract && underlyingMid > 0 ? contract.strike / underlyingMid : null,
atm_proximity: atmProximity,
aggressor_buy_ratio: clamp(numberFeature(packet, "nbbo_aggressive_buy_ratio")),
aggressor_sell_ratio: clamp(numberFeature(packet, "nbbo_aggressive_sell_ratio")),
aggressor_ratio: clamp(numberFeature(packet, "nbbo_aggressive_ratio")),
nbbo_coverage_ratio: clamp(numberFeature(packet, "nbbo_coverage_ratio")),
nbbo_inside_ratio: clamp(numberFeature(packet, "nbbo_inside_ratio")),
nbbo_stale_ratio: printCount > 0 ? clamp((staleCount + missingCount) / printCount) : 0,
quote_age_ms: quoteAge > 0 ? quoteAge : null,
venue_count: Math.max(1, Math.round(numberFeature(packet, "venue_count") || 1)),
inter_fill_ms_mean: printCount > 1 ? numberFeature(packet, "window_ms") / Math.max(1, printCount - 1) : null,
strike_count: strikeCount,
strike_concentration: strikeCount > 0 ? clamp(1 / strikeCount) : 0,
...(stringFeature(packet, "structure_type") ? { structure_type: stringFeature(packet, "structure_type") } : {}),
structure_legs: structureLegs,
same_size_leg_symmetry: clamp(numberFeature(packet, "same_size_leg_symmetry")),
net_directional_bias: clamp(
numberFeature(packet, "nbbo_aggressive_buy_ratio") - numberFeature(packet, "nbbo_aggressive_sell_ratio"),
-1,
1
),
synthetic_iv_shock: numberFeature(packet, "execution_iv_shock") || null,
spread_widening: numberFeature(packet, "nbbo_spread_z") || null,
underlying_move_bps: numberFeature(packet, "underlying_move_bps") || null,
days_to_event: eventTs > 0 ? (eventTs - referenceTs) / MS_PER_DAY : null,
expiry_after_event: eventTs > 0 && Number.isFinite(expiryTs) ? expiryTs >= eventTs : null,
pre_event_concentration: eventTs > 0 && eventTs >= referenceTs ? clamp(1 - (eventTs - referenceTs) / (21 * MS_PER_DAY)) : null,
special_print_ratio: printCount > 0 ? clamp(specialCount / printCount) : 0
};
};
const detectSuppression = (packet: FlowPacket, features: SmartMoneyFeatures): string[] => {
const reasons: string[] = [];
const conditions = String(packet.features.conditions ?? "")
.split(",")
.map((item) => item.trim().toUpperCase())
.filter(Boolean);
if (conditions.some((condition) => SPECIAL_CONDITIONS.has(condition)) || features.special_print_ratio >= 0.34) {
reasons.push("special_print_or_complex_context");
}
if (features.nbbo_coverage_ratio < 0.35 || features.nbbo_stale_ratio >= 0.5) {
reasons.push("stale_or_missing_quote_context");
}
if (features.nbbo_inside_ratio >= 0.7 && features.aggressor_ratio < 0.35) {
reasons.push("inside_market_or_cross_like_execution");
}
return reasons;
};
const evaluateProfiles = (
packet: FlowPacket,
features: SmartMoneyFeatures,
suppressed: string[]
): SmartMoneyProfileScore[] => {
const direction = inferDirection(packet);
const dte = features.dte_days ?? 999;
const structure = features.structure_type ?? "";
const isStructure = features.structure_legs >= 2 || Boolean(structure);
const buy = features.aggressor_buy_ratio;
const sell = features.aggressor_sell_ratio;
const premiumFactor = clamp(features.total_premium / 120_000);
const sizeFactor = clamp(features.total_size / 1800);
const burstFactor = clamp(features.print_count / 8);
const quality = clamp(features.nbbo_coverage_ratio - features.nbbo_stale_ratio);
const shortDatedOtm =
dte <= 7 && features.atm_proximity !== null && features.atm_proximity >= 0.05 && features.option_type === "C";
const nearAtm = features.atm_proximity !== null && features.atm_proximity <= 0.015;
const preEvent =
features.days_to_event !== null &&
features.days_to_event >= 0 &&
features.days_to_event <= 21 &&
features.expiry_after_event === true;
const scores = [
score(
"institutional_directional",
suppressed.length > 0 || shortDatedOtm
? 0.18
: 0.2 + premiumFactor * 0.25 + burstFactor * 0.18 + quality * 0.16 + (buy >= 0.58 || sell >= 0.58 ? 0.12 : 0),
direction,
[
"large_parent_event",
"directional_aggressor_mix",
...(shortDatedOtm ? ["retail_frenzy_guard"] : []),
...suppressed
]
),
score(
"retail_whale",
0.12 +
(shortDatedOtm ? 0.28 : 0) +
burstFactor * 0.18 +
clamp(features.synthetic_iv_shock ?? 0, 0, 0.2) +
(features.total_premium < 100_000 ? 0.1 : 0),
direction,
["short_dated_otm_attention_flow", "burst_print_pattern"]
),
score(
"event_driven",
0.12 + (preEvent ? 0.32 : 0) + premiumFactor * 0.14 + clamp(features.spread_widening ?? 0, 0, 0.16),
direction === "unknown" ? "neutral" : direction,
["event_calendar_alignment", "expiry_after_event", "pre_event_concentration"]
),
score(
"vol_seller",
0.12 + (sell >= 0.58 ? 0.24 : 0) + (structure === "straddle" || structure === "strangle" ? 0.2 : 0) + premiumFactor * 0.14,
"neutral",
["sell_side_premium", "short_vol_structure_evidence"]
),
score(
"arbitrage",
0.08 +
(isStructure ? 0.18 : 0) +
(features.same_size_leg_symmetry >= 0.7 ? 0.24 : 0) +
(Math.abs(features.net_directional_bias) <= 0.15 ? 0.18 : 0),
"neutral",
["matched_leg_symmetry", "near_flat_directional_exposure"]
),
score(
"hedge_reactive",
0.1 +
(dte <= 2 && nearAtm ? 0.32 : 0) +
clamp(Math.abs(features.underlying_move_bps ?? 0) / 80, 0, 0.18) +
sizeFactor * 0.12,
direction,
["short_dated_atm_gamma_context", "underlying_move_linkage"]
)
];
return scores.sort((a, b) => b.probability - a.probability);
};
export const buildSmartMoneyEventFromPacket = (packet: FlowPacket): SmartMoneyEvent => {
const features = buildFeatures(packet);
const suppressed = detectSuppression(packet, features);
const profileScores = evaluateProfiles(packet, features, suppressed);
const primary = profileScores[0] ?? null;
const abstained = !primary || primary.probability < 0.42 || suppressed.includes("stale_or_missing_quote_context");
const underlying = stringFeature(packet, "underlying_id") || parseContractId(features.option_contract_id ?? "")?.root || "UNKNOWN";
const eventKind = features.structure_legs >= 2 || stringFeature(packet, "packet_kind") === "structure"
? "multi_leg_event"
: "single_leg_event";
return SmartMoneyEventSchema.parse({
source_ts: packet.source_ts,
ingest_ts: packet.ingest_ts,
seq: packet.seq,
trace_id: `smartmoney:${packet.id}`,
event_id: `smartmoney:${eventKind}:${packet.id}`,
packet_ids: [packet.id],
member_print_ids: packet.members,
underlying_id: underlying,
event_kind: eventKind,
event_window_ms: features.window_ms,
features,
profile_scores: profileScores,
primary_profile_id: abstained ? null : primary?.profile_id ?? null,
primary_direction: abstained ? "unknown" : primary?.direction ?? "unknown",
abstained,
suppressed_reasons: suppressed
});
};
const LEGACY_PROFILE_MAP: Record<SmartMoneyProfileId, string> = {
institutional_directional: "smart_money_institutional_directional",
retail_whale: "smart_money_retail_whale",
event_driven: "smart_money_event_driven",
vol_seller: "smart_money_vol_seller",
arbitrage: "smart_money_arbitrage",
hedge_reactive: "smart_money_hedge_reactive"
};
export const deriveClassifierHitsFromSmartMoneyEvent = (event: SmartMoneyEvent): ClassifierHit[] => {
if (event.abstained || !event.primary_profile_id) {
return [];
}
return event.profile_scores
.filter((entry) => entry.profile_id === event.primary_profile_id || entry.probability >= 0.5)
.slice(0, 3)
.map((entry) => ({
classifier_id: LEGACY_PROFILE_MAP[entry.profile_id],
confidence: entry.probability,
direction: entry.direction,
explanations: [
`Profile ${entry.profile_id} probability ${(entry.probability * 100).toFixed(0)}%.`,
...entry.reasons,
...event.suppressed_reasons.map((reason) => `Suppression guard: ${reason}.`)
]
}));
};

View file

@ -0,0 +1,58 @@
import { describe, expect, it } from "bun:test";
import {
buildSmartMoneyEventFromPacket,
deriveClassifierHitsFromSmartMoneyEvent
} from "../src/parent-events";
import { buildFlowPacket } from "./helpers";
describe("smart money parent events", () => {
it("scores institutional directional parent events and derives legacy hits", () => {
const packet = buildFlowPacket({
id: "flowpacket:institutional",
source_ts: Date.parse("2025-01-15T15:00:00Z"),
features: {
option_contract_id: "SPY-2025-02-21-450-C",
underlying_id: "SPY",
count: 8,
window_ms: 450,
total_size: 2200,
total_premium: 180_000,
total_notional: 18_000_000,
nbbo_coverage_ratio: 0.92,
nbbo_aggressive_ratio: 0.82,
nbbo_aggressive_buy_ratio: 0.78,
nbbo_aggressive_sell_ratio: 0.04,
nbbo_inside_ratio: 0.08,
underlying_mid: 448
}
});
const event = buildSmartMoneyEventFromPacket(packet);
expect(event.event_kind).toBe("single_leg_event");
expect(event.primary_profile_id).toBe("institutional_directional");
expect(event.primary_direction).toBe("bullish");
const hits = deriveClassifierHitsFromSmartMoneyEvent(event);
expect(hits[0]?.classifier_id).toBe("smart_money_institutional_directional");
});
it("abstains when quote context is stale or missing", () => {
const packet = buildFlowPacket({
id: "flowpacket:stale",
features: {
option_contract_id: "SPY-2025-02-21-450-C",
count: 8,
window_ms: 450,
total_size: 2200,
total_premium: 180_000,
nbbo_coverage_ratio: 0.1,
nbbo_missing_count: 8
}
});
const event = buildSmartMoneyEventFromPacket(packet);
expect(event.abstained).toBe(true);
expect(event.primary_profile_id).toBeNull();
expect(event.suppressed_reasons).toContain("stale_or_missing_quote_context");
});
});