This commit is contained in:
parent
65139bf8d0
commit
44431c4e66
71 changed files with 2262 additions and 1173 deletions
|
|
@ -14,4 +14,3 @@ export const scoreAlert = (
|
|||
const severity = score >= 80 ? "high" : score >= 45 ? "medium" : "low";
|
||||
return { score, severity };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -573,10 +573,7 @@ const buildVerticalSpreadHit = (
|
|||
};
|
||||
};
|
||||
|
||||
const buildLadderHit = (
|
||||
packet: FlowPacket,
|
||||
config: ClassifierConfig
|
||||
): ClassifierHit | null => {
|
||||
const buildLadderHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierHit | null => {
|
||||
const structureType = getStringFeature(packet, "structure_type");
|
||||
if (structureType !== "ladder") {
|
||||
return null;
|
||||
|
|
@ -648,7 +645,8 @@ const buildRollHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierH
|
|||
}
|
||||
|
||||
const activity = getLargeActivity(packet, config);
|
||||
const qualifies = activity.totalPremium >= config.spikeMinPremium || activity.totalSize >= config.spikeMinSize;
|
||||
const qualifies =
|
||||
activity.totalPremium >= config.spikeMinPremium || activity.totalSize >= config.spikeMinSize;
|
||||
if (!qualifies) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -708,7 +706,9 @@ const buildRollHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierH
|
|||
|
||||
const expiryNote = hasExpiryPair
|
||||
? `Expiries: ${fromExpiry} -> ${toExpiry}${
|
||||
expiryDaysDelta !== null && expiryDaysDelta !== 0 ? ` (${Math.round(expiryDaysDelta)}d)` : ""
|
||||
expiryDaysDelta !== null && expiryDaysDelta !== 0
|
||||
? ` (${Math.round(expiryDaysDelta)}d)`
|
||||
: ""
|
||||
}.`
|
||||
: "Expiry pairing unavailable.";
|
||||
const strikeNote = hasStrikePair
|
||||
|
|
@ -850,9 +850,10 @@ export const evaluateClassifiers = (
|
|||
const packetKind = getStringFeature(packet, "packet_kind");
|
||||
const structureOnly = packetKind === "structure";
|
||||
|
||||
const contractId = typeof packet.features.option_contract_id === "string"
|
||||
? packet.features.option_contract_id
|
||||
: "";
|
||||
const contractId =
|
||||
typeof packet.features.option_contract_id === "string"
|
||||
? packet.features.option_contract_id
|
||||
: "";
|
||||
const contract = structureOnly ? null : parseContractId(contractId);
|
||||
const hits: ClassifierHit[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ const roundTo = (value: number, digits = 4): number => {
|
|||
return Number(value.toFixed(digits));
|
||||
};
|
||||
|
||||
export const classifyQuotePlacement = (
|
||||
price: number,
|
||||
join: EquityQuoteJoin
|
||||
): QuotePlacement => {
|
||||
export const classifyQuotePlacement = (price: number, join: EquityQuoteJoin): QuotePlacement => {
|
||||
if (!Number.isFinite(price)) {
|
||||
return "MISSING";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import {
|
|||
enqueueEquityPrintJoinInsert,
|
||||
enqueueFlowPacketInsert,
|
||||
enqueueInferredDarkInsert,
|
||||
enqueueSmartMoneyEventInsert,
|
||||
enqueueSmartMoneyEventInsert
|
||||
} from "@islandflow/storage";
|
||||
import {
|
||||
AlertEventSchema,
|
||||
|
|
@ -324,7 +324,9 @@ const buildPacketId = (cluster: ClusterState): string => {
|
|||
|
||||
const isExpectedShutdownNatsError = (error: unknown): boolean => {
|
||||
const code = getErrorCode(error);
|
||||
return runtimeState.shuttingDown && (code === "CONNECTION_DRAINING" || code === "CONNECTION_CLOSED");
|
||||
return (
|
||||
runtimeState.shuttingDown && (code === "CONNECTION_DRAINING" || code === "CONNECTION_CLOSED")
|
||||
);
|
||||
};
|
||||
|
||||
const createPlacementCounts = (): NbboPlacementCounts => ({
|
||||
|
|
@ -337,7 +339,14 @@ const createPlacementCounts = (): NbboPlacementCounts => ({
|
|||
stale: 0
|
||||
});
|
||||
|
||||
const SPECIAL_PRINT_CONDITIONS = new Set(["AUCTION", "CROSS", "OPENING", "CLOSING", "COMPLEX", "SPREAD"]);
|
||||
const SPECIAL_PRINT_CONDITIONS = new Set([
|
||||
"AUCTION",
|
||||
"CROSS",
|
||||
"OPENING",
|
||||
"CLOSING",
|
||||
"COMPLEX",
|
||||
"SPREAD"
|
||||
]);
|
||||
const SYNTHETIC_EVENT_CONDITION_RE = /^EVENT_(\d+)D$/i;
|
||||
|
||||
const normalizeConditions = (conditions: readonly string[] | undefined): string[] =>
|
||||
|
|
@ -460,11 +469,7 @@ const storeRecentRootLeg = (leg: LegEvidence, anchorTs: number): void => {
|
|||
recentLegsByRoot.set(key, next);
|
||||
};
|
||||
|
||||
const collectActiveLegs = (
|
||||
key: string,
|
||||
anchorTs: number,
|
||||
excludeId: string
|
||||
): LegEvidence[] => {
|
||||
const collectActiveLegs = (key: string, anchorTs: number, excludeId: string): LegEvidence[] => {
|
||||
const legs: LegEvidence[] = [];
|
||||
for (const [contractId, cluster] of clusters) {
|
||||
if (contractId === excludeId) {
|
||||
|
|
@ -485,11 +490,7 @@ const collectActiveLegs = (
|
|||
return legs;
|
||||
};
|
||||
|
||||
const collectActiveRootLegs = (
|
||||
key: string,
|
||||
anchorTs: number,
|
||||
excludeId: string
|
||||
): LegEvidence[] => {
|
||||
const collectActiveRootLegs = (key: string, anchorTs: number, excludeId: string): LegEvidence[] => {
|
||||
const legs: LegEvidence[] = [];
|
||||
for (const [contractId, cluster] of clusters) {
|
||||
if (contractId === excludeId) {
|
||||
|
|
@ -601,12 +602,19 @@ const applyDeliverPolicy = (
|
|||
const buildCluster = (print: OptionPrint): ClusterState => {
|
||||
const placements = createPlacementCounts();
|
||||
const normalizedConditions = normalizeConditions(print.conditions);
|
||||
const executionIv = typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv) ? print.execution_iv : null;
|
||||
const executionIv =
|
||||
typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv)
|
||||
? print.execution_iv
|
||||
: null;
|
||||
const executionUnderlyingMid =
|
||||
typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)
|
||||
typeof print.execution_underlying_mid === "number" &&
|
||||
Number.isFinite(print.execution_underlying_mid)
|
||||
? print.execution_underlying_mid
|
||||
: null;
|
||||
recordPlacement(placements, classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts)));
|
||||
recordPlacement(
|
||||
placements,
|
||||
classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts))
|
||||
);
|
||||
return {
|
||||
contractId: print.option_contract_id,
|
||||
underlyingId: print.underlying_id ?? null,
|
||||
|
|
@ -661,11 +669,18 @@ const updateCluster = (cluster: ClusterState, print: OptionPrint): ClusterState
|
|||
if (typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv)) {
|
||||
cluster.lastExecutionIv = print.execution_iv;
|
||||
cluster.minExecutionIv =
|
||||
cluster.minExecutionIv === null ? print.execution_iv : Math.min(cluster.minExecutionIv, print.execution_iv);
|
||||
cluster.minExecutionIv === null
|
||||
? print.execution_iv
|
||||
: Math.min(cluster.minExecutionIv, print.execution_iv);
|
||||
cluster.maxExecutionIv =
|
||||
cluster.maxExecutionIv === null ? print.execution_iv : Math.max(cluster.maxExecutionIv, print.execution_iv);
|
||||
cluster.maxExecutionIv === null
|
||||
? print.execution_iv
|
||||
: Math.max(cluster.maxExecutionIv, print.execution_iv);
|
||||
}
|
||||
if (typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)) {
|
||||
if (
|
||||
typeof print.execution_underlying_mid === "number" &&
|
||||
Number.isFinite(print.execution_underlying_mid)
|
||||
) {
|
||||
if (cluster.firstUnderlyingMid === null) {
|
||||
cluster.firstUnderlyingMid = print.execution_underlying_mid;
|
||||
}
|
||||
|
|
@ -686,11 +701,7 @@ type NbboJoin = {
|
|||
|
||||
const updateNbboCache = (nbbo: OptionNBBO): void => {
|
||||
const existing = nbboCache.get(nbbo.option_contract_id);
|
||||
if (
|
||||
!existing ||
|
||||
nbbo.ts > existing.ts ||
|
||||
(nbbo.ts === existing.ts && nbbo.seq >= existing.seq)
|
||||
) {
|
||||
if (!existing || nbbo.ts > existing.ts || (nbbo.ts === existing.ts && nbbo.seq >= existing.seq)) {
|
||||
nbboCache.set(nbbo.option_contract_id, nbbo);
|
||||
nbboCacheTouchedAt.set(nbbo.option_contract_id, Date.now());
|
||||
}
|
||||
|
|
@ -907,14 +918,18 @@ const flushCluster = async (
|
|||
features.special_print_count = cluster.specialPrintCount;
|
||||
}
|
||||
if (cluster.minExecutionIv !== null && cluster.maxExecutionIv !== null) {
|
||||
features.execution_iv_shock = roundTo(Math.max(0, cluster.maxExecutionIv - cluster.minExecutionIv));
|
||||
features.execution_iv_shock = roundTo(
|
||||
Math.max(0, cluster.maxExecutionIv - cluster.minExecutionIv)
|
||||
);
|
||||
}
|
||||
if (
|
||||
cluster.firstUnderlyingMid !== null &&
|
||||
cluster.lastUnderlyingMid !== null &&
|
||||
cluster.firstUnderlyingMid > 0
|
||||
) {
|
||||
const moveBps = ((cluster.lastUnderlyingMid - cluster.firstUnderlyingMid) / cluster.firstUnderlyingMid) * 10_000;
|
||||
const moveBps =
|
||||
((cluster.lastUnderlyingMid - cluster.firstUnderlyingMid) / cluster.firstUnderlyingMid) *
|
||||
10_000;
|
||||
features.underlying_move_bps = roundTo(moveBps);
|
||||
}
|
||||
const syntheticEventOffsetDays = parseSyntheticEventOffsetDays(cluster.conditions);
|
||||
|
|
@ -1004,7 +1019,13 @@ const flushCluster = async (
|
|||
const rollLegs = [currentLeg, ...rootCandidates];
|
||||
const rollSummary = summarizeStructure(rollLegs);
|
||||
if (rollSummary?.type === "roll") {
|
||||
await emitStructurePacketIfNeeded(js, batchWriter, rollLegs, rollSummary, currentLeg.contractId);
|
||||
await emitStructurePacketIfNeeded(
|
||||
js,
|
||||
batchWriter,
|
||||
rollLegs,
|
||||
rollSummary,
|
||||
currentLeg.contractId
|
||||
);
|
||||
}
|
||||
|
||||
storeRecentLeg(currentLeg, anchorTs);
|
||||
|
|
@ -1072,13 +1093,21 @@ const emitClassifiers = async (
|
|||
const underlyingId =
|
||||
typeof packet.features.underlying_id === "string"
|
||||
? packet.features.underlying_id
|
||||
: parseContractId(typeof packet.features.option_contract_id === "string" ? packet.features.option_contract_id : "")?.root;
|
||||
: parseContractId(
|
||||
typeof packet.features.option_contract_id === "string"
|
||||
? packet.features.option_contract_id
|
||||
: ""
|
||||
)?.root;
|
||||
const referenceTs =
|
||||
typeof packet.features.end_ts === "number" && Number.isFinite(packet.features.end_ts)
|
||||
? packet.features.end_ts
|
||||
: packet.source_ts;
|
||||
const eventCalendarMatch = underlyingId ? eventCalendarProvider.findNextEvent(underlyingId, referenceTs) : null;
|
||||
smartMoneyEvent = SmartMoneyEventSchema.parse(buildSmartMoneyEventFromPacket(packet, { eventCalendarMatch }));
|
||||
const eventCalendarMatch = underlyingId
|
||||
? eventCalendarProvider.findNextEvent(underlyingId, referenceTs)
|
||||
: null;
|
||||
smartMoneyEvent = SmartMoneyEventSchema.parse(
|
||||
buildSmartMoneyEventFromPacket(packet, { eventCalendarMatch })
|
||||
);
|
||||
enqueueSmartMoneyEventInsert(batchWriter, smartMoneyEvent);
|
||||
await publishJson(js, SUBJECT_SMART_MONEY_EVENTS, smartMoneyEvent);
|
||||
emitCounters.smartMoneyEvents += 1;
|
||||
|
|
@ -1282,20 +1311,29 @@ const run = async () => {
|
|||
|
||||
if (env.SMART_MONEY_EVENT_CALENDAR_PATH) {
|
||||
try {
|
||||
eventCalendarProvider = await loadEventCalendarProviderFromFile(env.SMART_MONEY_EVENT_CALENDAR_PATH);
|
||||
logger.info("smart money event calendar loaded", { path: env.SMART_MONEY_EVENT_CALENDAR_PATH });
|
||||
eventCalendarProvider = await loadEventCalendarProviderFromFile(
|
||||
env.SMART_MONEY_EVENT_CALENDAR_PATH
|
||||
);
|
||||
logger.info("smart money event calendar loaded", {
|
||||
path: env.SMART_MONEY_EVENT_CALENDAR_PATH
|
||||
});
|
||||
} catch (error) {
|
||||
eventCalendarProvider = createEmptyEventCalendarProvider();
|
||||
logger.warn("smart money event calendar unavailable; scoring will use neutral event features", {
|
||||
path: env.SMART_MONEY_EVENT_CALENDAR_PATH,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
logger.warn(
|
||||
"smart money event calendar unavailable; scoring will use neutral event features",
|
||||
{
|
||||
path: env.SMART_MONEY_EVENT_CALENDAR_PATH,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const redis = createRedisClient(env.REDIS_URL);
|
||||
redis.on("error", (error) => {
|
||||
logger.warn("redis client error", { error: error instanceof Error ? error.message : String(error) });
|
||||
logger.warn("redis client error", {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
});
|
||||
|
||||
await retry("redis connect", 120, 500, async () => {
|
||||
|
|
@ -1379,7 +1417,10 @@ const run = async () => {
|
|||
} else {
|
||||
try {
|
||||
const info = await jsm.consumers.info(STREAM_OPTION_SIGNAL_PRINTS, durableName);
|
||||
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
|
||||
if (
|
||||
info?.config?.deliver_policy &&
|
||||
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
|
||||
) {
|
||||
logger.warn("resetting consumer due to deliver policy change", {
|
||||
durable: durableName,
|
||||
current: info.config.deliver_policy,
|
||||
|
|
@ -1390,7 +1431,10 @@ const run = async () => {
|
|||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("not found")) {
|
||||
logger.warn("failed to inspect jetstream consumer", { durable: durableName, error: message });
|
||||
logger.warn("failed to inspect jetstream consumer", {
|
||||
durable: durableName,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1402,13 +1446,19 @@ const run = async () => {
|
|||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("not found")) {
|
||||
logger.warn("failed to reset jetstream consumer", { durable: nbboDurableName, error: message });
|
||||
logger.warn("failed to reset jetstream consumer", {
|
||||
durable: nbboDurableName,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const info = await jsm.consumers.info(STREAM_OPTION_NBBO, nbboDurableName);
|
||||
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
|
||||
if (
|
||||
info?.config?.deliver_policy &&
|
||||
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
|
||||
) {
|
||||
logger.warn("resetting consumer due to deliver policy change", {
|
||||
durable: nbboDurableName,
|
||||
current: info.config.deliver_policy,
|
||||
|
|
@ -1419,7 +1469,10 @@ const run = async () => {
|
|||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("not found")) {
|
||||
logger.warn("failed to inspect jetstream consumer", { durable: nbboDurableName, error: message });
|
||||
logger.warn("failed to inspect jetstream consumer", {
|
||||
durable: nbboDurableName,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1440,7 +1493,10 @@ const run = async () => {
|
|||
} else {
|
||||
try {
|
||||
const info = await jsm.consumers.info(STREAM_EQUITY_PRINTS, equityPrintDurableName);
|
||||
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
|
||||
if (
|
||||
info?.config?.deliver_policy &&
|
||||
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
|
||||
) {
|
||||
logger.warn("resetting consumer due to deliver policy change", {
|
||||
durable: equityPrintDurableName,
|
||||
current: info.config.deliver_policy,
|
||||
|
|
@ -1475,7 +1531,10 @@ const run = async () => {
|
|||
} else {
|
||||
try {
|
||||
const info = await jsm.consumers.info(STREAM_EQUITY_QUOTES, equityQuoteDurableName);
|
||||
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
|
||||
if (
|
||||
info?.config?.deliver_policy &&
|
||||
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
|
||||
) {
|
||||
logger.warn("resetting consumer due to deliver policy change", {
|
||||
durable: equityQuoteDurableName,
|
||||
current: info.config.deliver_policy,
|
||||
|
|
@ -1515,7 +1574,8 @@ const run = async () => {
|
|||
try {
|
||||
await jsm.consumers.delete(STREAM_OPTION_SIGNAL_PRINTS, durableName);
|
||||
} catch (deleteError) {
|
||||
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
const deleteMessage =
|
||||
deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
if (!deleteMessage.includes("not found")) {
|
||||
logger.warn("failed to delete jetstream consumer", {
|
||||
durable: durableName,
|
||||
|
|
@ -1551,7 +1611,8 @@ const run = async () => {
|
|||
try {
|
||||
await jsm.consumers.delete(STREAM_OPTION_NBBO, nbboDurableName);
|
||||
} catch (deleteError) {
|
||||
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
const deleteMessage =
|
||||
deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
if (!deleteMessage.includes("not found")) {
|
||||
logger.warn("failed to delete jetstream consumer", {
|
||||
durable: nbboDurableName,
|
||||
|
|
@ -1582,12 +1643,16 @@ const run = async () => {
|
|||
throw error;
|
||||
}
|
||||
|
||||
logger.warn("resetting jetstream consumer", { durable: equityPrintDurableName, error: message });
|
||||
logger.warn("resetting jetstream consumer", {
|
||||
durable: equityPrintDurableName,
|
||||
error: message
|
||||
});
|
||||
|
||||
try {
|
||||
await jsm.consumers.delete(STREAM_EQUITY_PRINTS, equityPrintDurableName);
|
||||
} catch (deleteError) {
|
||||
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
const deleteMessage =
|
||||
deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
if (!deleteMessage.includes("not found")) {
|
||||
logger.warn("failed to delete jetstream consumer", {
|
||||
durable: equityPrintDurableName,
|
||||
|
|
@ -1626,7 +1691,8 @@ const run = async () => {
|
|||
try {
|
||||
await jsm.consumers.delete(STREAM_EQUITY_QUOTES, equityQuoteDurableName);
|
||||
} catch (deleteError) {
|
||||
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
const deleteMessage =
|
||||
deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
if (!deleteMessage.includes("not found")) {
|
||||
logger.warn("failed to delete jetstream consumer", {
|
||||
durable: equityQuoteDurableName,
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ const getDteDays = (packet: FlowPacket): number | 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 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;
|
||||
|
|
@ -102,16 +104,26 @@ export type SmartMoneyParentEventOptions = {
|
|||
eventCalendarMatch?: EventCalendarMatch | null;
|
||||
};
|
||||
|
||||
const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions = {}): SmartMoneyFeatures => {
|
||||
const buildFeatures = (
|
||||
packet: FlowPacket,
|
||||
options: SmartMoneyParentEventOptions = {}
|
||||
): 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 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 strikeCount = Math.max(
|
||||
1,
|
||||
Math.round(numberFeature(packet, "structure_strikes") || (contract ? 1 : 0))
|
||||
);
|
||||
const specialCount = numberFeature(packet, "special_print_count");
|
||||
const calendarEventTs = options.eventCalendarMatch?.event_ts ?? null;
|
||||
const eventTs = calendarEventTs ?? numberFeature(packet, "corporate_event_ts");
|
||||
|
|
@ -119,7 +131,9 @@ const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions
|
|||
const expiryTs = contract ? Date.parse(`${contract.expiry}T00:00:00Z`) : Number.NaN;
|
||||
|
||||
const atmProximity =
|
||||
contract && underlyingMid > 0 ? Math.abs(contract.strike - underlyingMid) / underlyingMid : null;
|
||||
contract && underlyingMid > 0
|
||||
? Math.abs(contract.strike - underlyingMid) / underlyingMid
|
||||
: null;
|
||||
|
||||
return {
|
||||
contract_count: Math.max(1, structureLegs || 1),
|
||||
|
|
@ -143,14 +157,18 @@ const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions
|
|||
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,
|
||||
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") } : {}),
|
||||
...(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"),
|
||||
numberFeature(packet, "nbbo_aggressive_buy_ratio") -
|
||||
numberFeature(packet, "nbbo_aggressive_sell_ratio"),
|
||||
-1,
|
||||
1
|
||||
),
|
||||
|
|
@ -159,7 +177,10 @@ const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions
|
|||
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,
|
||||
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
|
||||
};
|
||||
};
|
||||
|
|
@ -170,7 +191,10 @@ const detectSuppression = (packet: FlowPacket, features: SmartMoneyFeatures): st
|
|||
.split(",")
|
||||
.map((item) => item.trim().toUpperCase())
|
||||
.filter(Boolean);
|
||||
if (conditions.some((condition) => SPECIAL_CONDITIONS.has(condition)) || features.special_print_ratio >= 0.34) {
|
||||
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) {
|
||||
|
|
@ -198,7 +222,10 @@ const evaluateProfiles = (
|
|||
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";
|
||||
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 &&
|
||||
|
|
@ -211,7 +238,11 @@ const evaluateProfiles = (
|
|||
"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),
|
||||
: 0.2 +
|
||||
premiumFactor * 0.25 +
|
||||
burstFactor * 0.18 +
|
||||
quality * 0.16 +
|
||||
(buy >= 0.58 || sell >= 0.58 ? 0.12 : 0),
|
||||
direction,
|
||||
[
|
||||
"large_parent_event",
|
||||
|
|
@ -232,13 +263,19 @@ const evaluateProfiles = (
|
|||
),
|
||||
score(
|
||||
"event_driven",
|
||||
0.12 + (preEvent ? 0.32 : 0) + premiumFactor * 0.14 + clamp(features.spread_widening ?? 0, 0, 0.16),
|
||||
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,
|
||||
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"]
|
||||
),
|
||||
|
|
@ -273,11 +310,16 @@ export const buildSmartMoneyEventFromPacket = (
|
|||
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";
|
||||
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,
|
||||
|
|
@ -292,8 +334,8 @@ export const buildSmartMoneyEventFromPacket = (
|
|||
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",
|
||||
primary_profile_id: abstained ? null : (primary?.profile_id ?? null),
|
||||
primary_direction: abstained ? "unknown" : (primary?.direction ?? "unknown"),
|
||||
abstained,
|
||||
suppressed_reasons: suppressed
|
||||
});
|
||||
|
|
@ -308,7 +350,9 @@ const LEGACY_PROFILE_MAP: Record<SmartMoneyProfileId, string> = {
|
|||
hedge_reactive: "smart_money_hedge_reactive"
|
||||
};
|
||||
|
||||
export const deriveClassifierHitsFromSmartMoneyEvent = (event: SmartMoneyEvent): ClassifierHit[] => {
|
||||
export const deriveClassifierHitsFromSmartMoneyEvent = (
|
||||
event: SmartMoneyEvent
|
||||
): ClassifierHit[] => {
|
||||
if (event.abstained || !event.primary_profile_id) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ type RollingWindowEntry = {
|
|||
};
|
||||
|
||||
const toNumbers = (values: string[]): number[] => {
|
||||
return values
|
||||
.map((value) => Number(value))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
return values.map((value) => Number(value)).filter((value) => Number.isFinite(value));
|
||||
};
|
||||
|
||||
export const computeStats = (values: number[]): { mean: number; stddev: number; count: number } => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import type { FlowPacket, SmartMoneyDirection, SmartMoneyEvent, SmartMoneyProfileId } from "@islandflow/types";
|
||||
import type {
|
||||
FlowPacket,
|
||||
SmartMoneyDirection,
|
||||
SmartMoneyEvent,
|
||||
SmartMoneyProfileId
|
||||
} from "@islandflow/types";
|
||||
import { buildSmartMoneyEventFromPacket, type SmartMoneyParentEventOptions } from "./parent-events";
|
||||
|
||||
export type SmartMoneyLabel = {
|
||||
|
|
@ -115,8 +120,12 @@ export const compareSmartMoneyReplayOutputs = (
|
|||
liveEvents: SmartMoneyEvent[],
|
||||
batchEvents: SmartMoneyEvent[]
|
||||
): ReplayConsistencyReport => {
|
||||
const liveById = new Map(liveEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)]));
|
||||
const batchById = new Map(batchEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)]));
|
||||
const liveById = new Map(
|
||||
liveEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)])
|
||||
);
|
||||
const batchById = new Map(
|
||||
batchEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)])
|
||||
);
|
||||
const ids = [...new Set([...liveById.keys(), ...batchById.keys()])].sort();
|
||||
const mismatches: ReplayConsistencyMismatch[] = [];
|
||||
|
||||
|
|
@ -153,7 +162,9 @@ export const evaluateSmartMoneyEvents = (
|
|||
const labelsById = new Map(labels.map((label) => [label.event_id, label]));
|
||||
const labeledEvents = events
|
||||
.map((event) => ({ event, label: labelsById.get(event.event_id) }))
|
||||
.filter((entry): entry is { event: SmartMoneyEvent; label: SmartMoneyLabel } => Boolean(entry.label));
|
||||
.filter((entry): entry is { event: SmartMoneyEvent; label: SmartMoneyLabel } =>
|
||||
Boolean(entry.label)
|
||||
);
|
||||
|
||||
const emitted = events.filter((event) => !event.abstained && event.primary_profile_id);
|
||||
const profilePrecision: SmartMoneyEvaluationReport["profile_precision"] = {};
|
||||
|
|
@ -163,7 +174,8 @@ export const evaluateSmartMoneyEvents = (
|
|||
const predicted = labeledEvents.filter((entry) => entry.event.primary_profile_id === profile);
|
||||
const actual = labeledEvents.filter((entry) => entry.label.profile_id === profile);
|
||||
const truePositive = predicted.filter((entry) => entry.label.profile_id === profile).length;
|
||||
profilePrecision[profile] = predicted.length > 0 ? round(truePositive / predicted.length) : null;
|
||||
profilePrecision[profile] =
|
||||
predicted.length > 0 ? round(truePositive / predicted.length) : null;
|
||||
profileRecall[profile] = actual.length > 0 ? round(truePositive / actual.length) : null;
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +187,10 @@ export const evaluateSmartMoneyEvents = (
|
|||
labeled_count: labeledEvents.length,
|
||||
emitted_count: emitted.length,
|
||||
abstained_count: events.filter((event) => event.abstained).length,
|
||||
abstention_rate: events.length > 0 ? round(events.filter((event) => event.abstained).length / events.length) : 0,
|
||||
abstention_rate:
|
||||
events.length > 0
|
||||
? round(events.filter((event) => event.abstained).length / events.length)
|
||||
: 0,
|
||||
profile_precision: profilePrecision,
|
||||
profile_recall: profileRecall,
|
||||
calibration,
|
||||
|
|
@ -195,7 +210,9 @@ const buildCalibration = (
|
|||
}));
|
||||
|
||||
for (const { event, label } of entries) {
|
||||
const probability = event.profile_scores.find((entry) => entry.profile_id === event.primary_profile_id)?.probability ?? 0;
|
||||
const probability =
|
||||
event.profile_scores.find((entry) => entry.profile_id === event.primary_profile_id)
|
||||
?.probability ?? 0;
|
||||
const index = Math.min(bucketCount - 1, Math.floor(probability * bucketCount));
|
||||
buckets[index].probabilities.push(probability);
|
||||
if (!event.abstained && event.primary_profile_id === label.profile_id) {
|
||||
|
|
@ -209,9 +226,13 @@ const buildCalibration = (
|
|||
count: bucket.probabilities.length,
|
||||
average_probability:
|
||||
bucket.probabilities.length > 0
|
||||
? round(bucket.probabilities.reduce((sum, value) => sum + value, 0) / bucket.probabilities.length)
|
||||
? round(
|
||||
bucket.probabilities.reduce((sum, value) => sum + value, 0) /
|
||||
bucket.probabilities.length
|
||||
)
|
||||
: 0,
|
||||
accuracy: bucket.probabilities.length > 0 ? round(bucket.correct / bucket.probabilities.length) : null
|
||||
accuracy:
|
||||
bucket.probabilities.length > 0 ? round(bucket.correct / bucket.probabilities.length) : null
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
@ -223,7 +244,10 @@ const buildEconomicSanity = (
|
|||
sign: directionalSign(event.primary_direction),
|
||||
realized: label.realized_return_bps
|
||||
}))
|
||||
.filter((entry): entry is { sign: number; realized: number } => entry.sign !== 0 && Number.isFinite(entry.realized));
|
||||
.filter(
|
||||
(entry): entry is { sign: number; realized: number } =>
|
||||
entry.sign !== 0 && Number.isFinite(entry.realized)
|
||||
);
|
||||
|
||||
if (directional.length === 0) {
|
||||
return {
|
||||
|
|
@ -236,7 +260,12 @@ const buildEconomicSanity = (
|
|||
const signedReturns = directional.map((entry) => entry.sign * entry.realized);
|
||||
return {
|
||||
directional_count: directional.length,
|
||||
direction_hit_rate: round(signedReturns.filter((value) => value > 0).length / directional.length),
|
||||
average_signed_return_bps: round(signedReturns.reduce((sum, value) => sum + value, 0) / signedReturns.length, 2)
|
||||
direction_hit_rate: round(
|
||||
signedReturns.filter((value) => value > 0).length / directional.length
|
||||
),
|
||||
average_signed_return_bps: round(
|
||||
signedReturns.reduce((sum, value) => sum + value, 0) / signedReturns.length,
|
||||
2
|
||||
)
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -134,7 +134,9 @@ const dayDiff = (from: string | null, to: string | null): number | null => {
|
|||
};
|
||||
|
||||
const sameSizeLegSymmetry = (legs: LegEvidence[]): number => {
|
||||
const sizes = legs.map((leg) => leg.totalSize).filter((value) => Number.isFinite(value) && value > 0);
|
||||
const sizes = legs
|
||||
.map((leg) => leg.totalSize)
|
||||
.filter((value) => Number.isFinite(value) && value > 0);
|
||||
if (sizes.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -146,7 +148,10 @@ const sameSizeLegSymmetry = (legs: LegEvidence[]): number => {
|
|||
return min / max;
|
||||
};
|
||||
|
||||
export const shouldEmitStructurePacket = (legs: LegEvidence[], currentLegContractId: string): boolean => {
|
||||
export const shouldEmitStructurePacket = (
|
||||
legs: LegEvidence[],
|
||||
currentLegContractId: string
|
||||
): boolean => {
|
||||
if (legs.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -226,7 +231,8 @@ export const planStructurePacket = (
|
|||
const totalSize = legs.reduce((sum, leg) => sum + leg.totalSize, 0);
|
||||
const count = legs.reduce((sum, leg) => sum + leg.members.length, 0);
|
||||
const placements = mergePlacements(legs);
|
||||
const placementTotal = placements.aa + placements.a + placements.b + placements.bb + placements.mid;
|
||||
const placementTotal =
|
||||
placements.aa + placements.a + placements.b + placements.bb + placements.mid;
|
||||
const aggressiveTotal = placements.aa + placements.a + placements.b + placements.bb;
|
||||
const aggressiveBuy = placements.aa + placements.a;
|
||||
const aggressiveSell = placements.bb + placements.b;
|
||||
|
|
@ -235,7 +241,10 @@ export const planStructurePacket = (
|
|||
const nbboAggressiveSellRatio = aggressiveTotal > 0 ? aggressiveSell / aggressiveTotal : 0;
|
||||
const nbboAggressiveRatio = placementTotal > 0 ? aggressiveTotal / placementTotal : 0;
|
||||
|
||||
const source_ts = legs.reduce((min, leg) => Math.min(min, leg.source_ts), Number.POSITIVE_INFINITY);
|
||||
const source_ts = legs.reduce(
|
||||
(min, leg) => Math.min(min, leg.source_ts),
|
||||
Number.POSITIVE_INFINITY
|
||||
);
|
||||
const ingest_ts = legs.reduce((max, leg) => Math.max(max, leg.ingest_ts), 0);
|
||||
const seq = legs.reduce((max, leg) => Math.max(max, leg.seq), 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ export const summarizeStructure = (legs: ContractLeg[]): StructureSummary | null
|
|||
legs: legs.length,
|
||||
strikes: strikes.length,
|
||||
strikeSpan,
|
||||
rights: rights.size === 2 ? "C/P" : Array.from(rights)[0] ?? "",
|
||||
contractIds: legs.map((leg) => leg.contractId).slice().sort()
|
||||
rights: rights.size === 2 ? "C/P" : (Array.from(rights)[0] ?? ""),
|
||||
contractIds: legs
|
||||
.map((leg) => leg.contractId)
|
||||
.slice()
|
||||
.sort()
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -293,4 +293,3 @@ describe("compute classifiers", () => {
|
|||
expect(hit!.explanations[0]).toMatch(/Consistent with/i);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,16 +17,18 @@ export const TEST_CLASSIFIER_CONFIG: ClassifierConfig = {
|
|||
zeroDteMinSize: 400
|
||||
};
|
||||
|
||||
export const buildFlowPacket = (opts: {
|
||||
id?: string;
|
||||
source_ts?: number;
|
||||
ingest_ts?: number;
|
||||
seq?: number;
|
||||
trace_id?: string;
|
||||
members?: string[];
|
||||
features?: FlowPacket["features"];
|
||||
join_quality?: FlowPacket["join_quality"];
|
||||
} = {}): FlowPacket => {
|
||||
export const buildFlowPacket = (
|
||||
opts: {
|
||||
id?: string;
|
||||
source_ts?: number;
|
||||
ingest_ts?: number;
|
||||
seq?: number;
|
||||
trace_id?: string;
|
||||
members?: string[];
|
||||
features?: FlowPacket["features"];
|
||||
join_quality?: FlowPacket["join_quality"];
|
||||
} = {}
|
||||
): FlowPacket => {
|
||||
const id = opts.id ?? "flowpacket:test";
|
||||
const source_ts = opts.source_ts ?? Date.parse("2025-01-01T14:30:00Z");
|
||||
const ingest_ts = opts.ingest_ts ?? source_ts;
|
||||
|
|
@ -66,4 +68,3 @@ export const buildFlowPacket = (opts: {
|
|||
export const getHit = (hits: ClassifierHit[], id: string): ClassifierHit | null => {
|
||||
return hits.find((hit) => hit.classifier_id === id) ?? null;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ const placements = (overrides?: Partial<LegEvidence["placements"]>): LegEvidence
|
|||
...overrides
|
||||
});
|
||||
|
||||
const leg = (input: Partial<LegEvidence> & Pick<LegEvidence, "contractId" | "right" | "strike">): LegEvidence => {
|
||||
const leg = (
|
||||
input: Partial<LegEvidence> & Pick<LegEvidence, "contractId" | "right" | "strike">
|
||||
): LegEvidence => {
|
||||
return {
|
||||
contractId: input.contractId,
|
||||
root: "SPY",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue