Add smart-money option signal path and tape filters

This commit is contained in:
dirtydishes 2026-04-28 16:29:44 -04:00
parent 758f111d7e
commit 27b0a399e6
23 changed files with 1827 additions and 175 deletions

View file

@ -1,4 +1,5 @@
import { z } from "zod";
import { OptionNbboSideSchema, OptionTypeSchema, OptionsSignalModeSchema } from "./options-flow";
export const EventMetaSchema = z.object({
source_ts: z.number().int().nonnegative(),
@ -16,7 +17,18 @@ export const OptionPrintSchema = EventMetaSchema.merge(
price: z.number().nonnegative(),
size: z.number().int().positive(),
exchange: z.string().min(1),
conditions: z.array(z.string().min(1)).optional()
conditions: z.array(z.string().min(1)).optional(),
underlying_id: z.preprocess((value) => (value === null ? undefined : value), z.string().min(1).optional()),
option_type: z.preprocess((value) => (value === null ? undefined : value), OptionTypeSchema.optional()),
notional: z.preprocess((value) => (value === null ? undefined : value), z.number().nonnegative().optional()),
nbbo_side: z.preprocess((value) => (value === null ? undefined : value), OptionNbboSideSchema.optional()),
is_etf: z.preprocess((value) => (value === null ? undefined : value), z.boolean().optional()),
signal_pass: z.preprocess((value) => (value === null ? undefined : value), z.boolean().optional()),
signal_reasons: z.array(z.string().min(1)).optional(),
signal_profile: z.preprocess(
(value) => (value === null ? undefined : value),
OptionsSignalModeSchema.optional()
)
})
);

View file

@ -1,3 +1,4 @@
export * from "./events";
export * from "./live";
export * from "./options-flow";
export * from "./sp500";

View file

@ -10,6 +10,10 @@ import {
OptionNBBOSchema,
OptionPrintSchema
} from "./events";
import {
OptionFlowFiltersSchema,
optionFlowFilterKey
} from "./options-flow";
export const CursorSchema = z.object({
ts: z.number().int().nonnegative(),
@ -47,7 +51,15 @@ export type LiveGenericChannel = z.infer<typeof LiveGenericChannelSchema>;
export const LiveSubscriptionSchema = z.discriminatedUnion("channel", [
z.object({
channel: LiveGenericChannelSchema
channel: z.literal("options"),
filters: OptionFlowFiltersSchema.optional()
}),
z.object({
channel: z.literal("flow"),
filters: OptionFlowFiltersSchema.optional()
}),
z.object({
channel: z.enum(["nbbo", "equities", "equity-joins", "classifier-hits", "alerts", "inferred-dark"])
}),
z.object({
channel: z.literal("equity-candles"),
@ -165,6 +177,9 @@ export type LiveServerMessage = z.infer<typeof LiveServerMessageSchema>;
export const getSubscriptionKey = (subscription: LiveSubscription): string => {
switch (subscription.channel) {
case "options":
case "flow":
return `${subscription.channel}|${optionFlowFilterKey(subscription.filters)}`;
case "equity-candles":
return `${subscription.channel}|${subscription.underlying_id}|${subscription.interval_ms}`;
case "equity-overlay":

View file

@ -0,0 +1,464 @@
import { z } from "zod";
import type { FlowPacket, OptionNBBO, OptionPrint } from "./events";
export const SyntheticMarketModeSchema = z.enum(["realistic", "active", "firehose"]);
export type SyntheticMarketMode = z.infer<typeof SyntheticMarketModeSchema>;
export const OptionTypeSchema = z.enum(["call", "put"]);
export type OptionType = z.infer<typeof OptionTypeSchema>;
export const OptionNbboSideSchema = z.enum(["AA", "A", "MID", "B", "BB", "MISSING", "STALE"]);
export type OptionNbboSide = z.infer<typeof OptionNbboSideSchema>;
export const OptionFlowViewSchema = z.enum(["signal", "raw"]);
export type OptionFlowView = z.infer<typeof OptionFlowViewSchema>;
export const OptionSecurityTypeSchema = z.enum(["stock", "etf"]);
export type OptionSecurityType = z.infer<typeof OptionSecurityTypeSchema>;
export const OptionsSignalModeSchema = z.enum(["smart-money", "balanced", "all"]);
export type OptionsSignalMode = z.infer<typeof OptionsSignalModeSchema>;
export const OptionFlowFiltersSchema = z.object({
view: OptionFlowViewSchema.optional(),
securityTypes: z.array(OptionSecurityTypeSchema).optional(),
nbboSides: z.array(OptionNbboSideSchema).optional(),
optionTypes: z.array(OptionTypeSchema).optional(),
minNotional: z.number().nonnegative().optional()
});
export type OptionFlowFilters = z.infer<typeof OptionFlowFiltersSchema>;
export type ParsedOptionContract = {
root: string;
expiry: string;
strike: number;
right: "C" | "P";
};
export type SyntheticModeResolution = {
market: SyntheticMarketMode;
options: SyntheticMarketMode;
equities: SyntheticMarketMode;
};
export type OptionsSignalConfig = {
mode: OptionsSignalMode;
minNotional: number;
etfMinNotional: number;
bidSideMinNotional: number;
midMinNotional: number;
missingNbboMinNotional: number;
largePrintMinSize: number;
largePrintMinNotional: number;
sweepMinNotional: number;
autoKeepMinNotional: number;
nbboMaxAgeMs: number;
etfUnderlyings: Set<string>;
};
export type DerivedOptionPrintMetadata = {
underlying_id?: string;
option_type?: OptionType;
notional?: number;
nbbo_side?: OptionNbboSide;
is_etf?: boolean;
};
export type OptionSignalDecision = {
signalPass: boolean;
signalReasons: string[];
signalProfile: OptionsSignalMode;
};
const parseDashedContract = (value: string): ParsedOptionContract | null => {
const parts = value.split("-");
if (parts.length < 6) {
return null;
}
const rightRaw = parts.at(-1) ?? "";
if (rightRaw !== "C" && rightRaw !== "P") {
return null;
}
const strikeRaw = parts.at(-2) ?? "";
const strike = Number(strikeRaw);
const expiryParts = parts.slice(-5, -2);
const expiry = expiryParts.join("-");
const root = parts.slice(0, -5).join("-");
if (!root || !expiry || !Number.isFinite(strike)) {
return null;
}
return {
root,
expiry,
strike,
right: rightRaw
};
};
const parseOccContract = (value: string): ParsedOptionContract | null => {
if (value.length < 15) {
return null;
}
const tail = value.slice(-15);
const root = value.slice(0, -15).trim();
const expiryRaw = tail.slice(0, 6);
const right = tail.slice(6, 7);
const strikeRaw = tail.slice(7);
if (!/^\d{6}$/.test(expiryRaw) || !/^\d{8}$/.test(strikeRaw)) {
return null;
}
if (right !== "C" && right !== "P") {
return null;
}
const year = 2000 + Number(expiryRaw.slice(0, 2));
const month = Number(expiryRaw.slice(2, 4)) - 1;
const day = Number(expiryRaw.slice(4, 6));
const expiryDate = new Date(Date.UTC(year, month, day));
const expiry = expiryDate.toISOString().slice(0, 10);
const strike = Number(strikeRaw) / 1000;
if (!root || !Number.isFinite(strike)) {
return null;
}
return {
root,
expiry,
strike,
right
};
};
export const parseOptionContractId = (value: string | undefined): ParsedOptionContract | null => {
if (!value) {
return null;
}
return parseDashedContract(value) ?? parseOccContract(value);
};
export const resolveSyntheticMarketModes = (input: {
syntheticMarketMode?: string | null | undefined;
syntheticOptionsMode?: string | null | undefined;
syntheticEquitiesMode?: string | null | undefined;
}): SyntheticModeResolution => {
const market = SyntheticMarketModeSchema.catch("realistic").parse(
input.syntheticMarketMode ?? "realistic"
);
const options = SyntheticMarketModeSchema.catch(market).parse(
input.syntheticOptionsMode ?? market
);
const equities = SyntheticMarketModeSchema.catch(market).parse(
input.syntheticEquitiesMode ?? market
);
return { market, options, equities };
};
export const classifyOptionNbboSide = (
price: number,
quote: Pick<OptionNBBO, "bid" | "ask" | "ts"> | null | undefined,
tradeTs: number,
maxAgeMs: number
): OptionNbboSide => {
if (!quote || !Number.isFinite(price)) {
return "MISSING";
}
const bid = quote.bid;
const ask = quote.ask;
if (!Number.isFinite(bid) || !Number.isFinite(ask) || ask <= 0) {
return "MISSING";
}
const ageMs = Math.abs(tradeTs - quote.ts);
if (ageMs > maxAgeMs) {
return "STALE";
}
const spread = Math.max(0, ask - bid);
const epsilon = Math.max(0.01, spread * 0.05);
if (price > ask + epsilon) {
return "AA";
}
if (price >= ask - epsilon) {
return "A";
}
if (price < bid - epsilon) {
return "BB";
}
if (price <= bid + epsilon) {
return "B";
}
return "MID";
};
export const deriveOptionPrintMetadata = (
print: Pick<OptionPrint, "option_contract_id" | "price" | "size" | "ts">,
quote: Pick<OptionNBBO, "bid" | "ask" | "ts"> | null | undefined,
config: Pick<OptionsSignalConfig, "nbboMaxAgeMs" | "etfUnderlyings">
): DerivedOptionPrintMetadata => {
const parsed = parseOptionContractId(print.option_contract_id);
const underlying = parsed?.root?.toUpperCase();
const optionType = parsed?.right === "C" ? "call" : parsed?.right === "P" ? "put" : undefined;
const notional = Number.isFinite(print.price) && Number.isFinite(print.size)
? Number((print.price * print.size * 100).toFixed(2))
: undefined;
return {
underlying_id: underlying,
option_type: optionType,
notional,
nbbo_side: classifyOptionNbboSide(print.price, quote, print.ts, config.nbboMaxAgeMs),
is_etf: underlying ? config.etfUnderlyings.has(underlying) : undefined
};
};
const hasCondition = (conditions: string[] | undefined, expected: string): boolean => {
return (conditions ?? []).some((condition) => condition.toUpperCase() === expected);
};
const balancedThresholds = (config: OptionsSignalConfig): OptionsSignalConfig => ({
...config,
minNotional: Math.min(config.minNotional, 5_000),
etfMinNotional: Math.min(config.etfMinNotional, 25_000),
bidSideMinNotional: Math.min(config.bidSideMinNotional, 15_000),
midMinNotional: Math.min(config.midMinNotional, 12_500),
missingNbboMinNotional: Math.min(config.missingNbboMinNotional, 25_000),
sweepMinNotional: Math.min(config.sweepMinNotional, 15_000),
autoKeepMinNotional: Math.min(config.autoKeepMinNotional, 75_000)
});
export const evaluateOptionSignal = (
print: Pick<
OptionPrint,
"size" | "conditions" | "signal_profile" | "underlying_id" | "option_type" | "notional" | "nbbo_side" | "is_etf"
>,
baseConfig: OptionsSignalConfig
): OptionSignalDecision => {
const mode = print.signal_profile ?? baseConfig.mode;
if (mode === "all") {
return {
signalPass: true,
signalReasons: ["mode:all"],
signalProfile: "all"
};
}
const config = mode === "balanced" ? balancedThresholds(baseConfig) : baseConfig;
const reasons: string[] = [];
const notional = print.notional ?? 0;
const side = print.nbbo_side ?? "MISSING";
const isSweepOrIso = hasCondition(print.conditions, "SWEEP") || hasCondition(print.conditions, "ISO");
if (notional < config.minNotional) {
return {
signalPass: false,
signalReasons: ["reject:min-notional"],
signalProfile: mode
};
}
if (notional >= config.autoKeepMinNotional) {
reasons.push("keep:auto-large");
}
if (print.is_etf && notional < config.etfMinNotional) {
return {
signalPass: false,
signalReasons: ["reject:etf-min-notional"],
signalProfile: mode
};
}
if ((side === "B" || side === "BB") && notional < config.bidSideMinNotional) {
return {
signalPass: false,
signalReasons: ["reject:bid-side-min-notional"],
signalProfile: mode
};
}
if (side === "MID" && !isSweepOrIso && notional < config.midMinNotional) {
return {
signalPass: false,
signalReasons: ["reject:mid-min-notional"],
signalProfile: mode
};
}
if ((side === "MISSING" || side === "STALE") && notional < config.missingNbboMinNotional) {
return {
signalPass: false,
signalReasons: ["reject:missing-nbbo-min-notional"],
signalProfile: mode
};
}
if ((side === "A" || side === "AA") && notional >= config.minNotional) {
reasons.push("keep:ask-side");
}
if (isSweepOrIso && notional >= config.sweepMinNotional) {
reasons.push("keep:sweep-or-iso");
}
if (print.size >= config.largePrintMinSize && notional >= config.largePrintMinNotional) {
reasons.push("keep:large-contract-count");
}
if (reasons.length === 0) {
return {
signalPass: false,
signalReasons: ["reject:no-signal-rule"],
signalProfile: mode
};
}
return {
signalPass: true,
signalReasons: reasons,
signalProfile: mode
};
};
const sortStrings = (values: string[] | undefined): string[] | undefined => {
if (!values || values.length === 0) {
return undefined;
}
return [...new Set(values)].sort();
};
export const normalizeOptionFlowFilters = (
filters: OptionFlowFilters | undefined
): OptionFlowFilters | undefined => {
if (!filters) {
return undefined;
}
return {
view: filters.view,
securityTypes: sortStrings(filters.securityTypes) as OptionSecurityType[] | undefined,
nbboSides: sortStrings(filters.nbboSides) as OptionNbboSide[] | undefined,
optionTypes: sortStrings(filters.optionTypes) as OptionType[] | undefined,
minNotional:
typeof filters.minNotional === "number" && Number.isFinite(filters.minNotional)
? filters.minNotional
: undefined
};
};
export const optionFlowFilterKey = (filters: OptionFlowFilters | undefined): string => {
return JSON.stringify(normalizeOptionFlowFilters(filters) ?? {});
};
export const matchesOptionPrintFilters = (
print: Pick<OptionPrint, "is_etf" | "nbbo_side" | "option_type" | "notional" | "signal_pass">,
filters: OptionFlowFilters | undefined
): boolean => {
if (!filters) {
return true;
}
const view = filters.view ?? "signal";
if (view === "signal" && print.signal_pass === false) {
return false;
}
if (filters.securityTypes?.length) {
const securityType: OptionSecurityType = print.is_etf ? "etf" : "stock";
if (!filters.securityTypes.includes(securityType)) {
return false;
}
}
if (filters.nbboSides?.length) {
const side = print.nbbo_side ?? "MISSING";
if (!filters.nbboSides.includes(side)) {
return false;
}
}
if (filters.optionTypes?.length) {
const optionType = print.option_type;
if (!optionType || !filters.optionTypes.includes(optionType)) {
return false;
}
}
if (typeof filters.minNotional === "number" && (print.notional ?? 0) < filters.minNotional) {
return false;
}
return true;
};
export const matchesFlowPacketFilters = (
packet: FlowPacket,
filters: OptionFlowFilters | undefined
): boolean => {
if (!filters) {
return true;
}
const features = packet.features ?? {};
const totalNotional = typeof features.total_notional === "number" ? features.total_notional : Number(features.total_notional ?? 0);
if (typeof filters.minNotional === "number" && (!Number.isFinite(totalNotional) || totalNotional < filters.minNotional)) {
return false;
}
if (filters.securityTypes?.length) {
const isEtf = typeof features.is_etf === "boolean" ? features.is_etf : features.is_etf === 1;
const securityType: OptionSecurityType = isEtf ? "etf" : "stock";
if (!filters.securityTypes.includes(securityType)) {
return false;
}
}
if (filters.optionTypes?.length) {
const optionType =
typeof features.option_type === "string"
? features.option_type
: typeof features.structure_rights === "string"
? features.structure_rights.toLowerCase()
: null;
if (
!optionType ||
!filters.optionTypes.some((selected) => optionType.includes(selected))
) {
return false;
}
}
if (filters.nbboSides?.length) {
const sideToFeature: Record<OptionNbboSide, string> = {
AA: "nbbo_aa_count",
A: "nbbo_a_count",
MID: "nbbo_mid_count",
B: "nbbo_b_count",
BB: "nbbo_bb_count",
MISSING: "nbbo_missing_count",
STALE: "nbbo_stale_count"
};
const matchesSide = filters.nbboSides.some((side) => {
const value = features[sideToFeature[side]];
return typeof value === "number" ? value > 0 : Number(value ?? 0) > 0;
});
if (!matchesSide) {
return false;
}
}
return true;
};

View file

@ -8,7 +8,21 @@ import {
describe("live protocol types", () => {
it("builds stable keys for generic and parameterized subscriptions", () => {
expect(getSubscriptionKey({ channel: "flow" })).toBe("flow");
expect(getSubscriptionKey({ channel: "flow" })).toBe("flow|{}");
expect(
getSubscriptionKey({
channel: "options",
filters: {
view: "signal",
securityTypes: ["stock"],
nbboSides: ["A", "AA"],
optionTypes: ["call", "put"],
minNotional: 25000
}
})
).toBe(
'options|{"view":"signal","securityTypes":["stock"],"nbboSides":["A","AA"],"optionTypes":["call","put"],"minNotional":25000}'
);
expect(
getSubscriptionKey({
channel: "equity-candles",
@ -25,7 +39,7 @@ describe("live protocol types", () => {
const parsed = LiveClientMessageSchema.parse({
op: "subscribe",
subscriptions: [
{ channel: "flow" },
{ channel: "flow", filters: { nbboSides: ["AA", "A"], minNotional: 50000 } },
{ channel: "equity-candles", underlying_id: "SPY", interval_ms: 60000 }
]
});

View file

@ -0,0 +1,132 @@
import { describe, expect, it } from "bun:test";
import {
deriveOptionPrintMetadata,
evaluateOptionSignal,
resolveSyntheticMarketModes,
type OptionsSignalConfig
} from "../src/options-flow";
const baseConfig: OptionsSignalConfig = {
mode: "smart-money",
minNotional: 10_000,
etfMinNotional: 50_000,
bidSideMinNotional: 25_000,
midMinNotional: 20_000,
missingNbboMinNotional: 50_000,
largePrintMinSize: 500,
largePrintMinNotional: 10_000,
sweepMinNotional: 25_000,
autoKeepMinNotional: 100_000,
nbboMaxAgeMs: 1_500,
etfUnderlyings: new Set(["SPY", "QQQ"])
};
describe("options-flow helpers", () => {
it("resolves synthetic modes with per-service overrides", () => {
expect(
resolveSyntheticMarketModes({
syntheticMarketMode: "active",
syntheticOptionsMode: "firehose"
})
).toEqual({
market: "active",
options: "firehose",
equities: "active"
});
});
it("derives underlying, notional, nbbo side, and etf metadata", () => {
const metadata = deriveOptionPrintMetadata(
{
option_contract_id: "SPY-2025-01-17-450-C",
price: 2.5,
size: 100,
ts: 5_000
},
{
bid: 2.3,
ask: 2.5,
ts: 4_500
},
baseConfig
);
expect(metadata.underlying_id).toBe("SPY");
expect(metadata.option_type).toBe("call");
expect(metadata.notional).toBe(25_000);
expect(metadata.nbbo_side).toBe("A");
expect(metadata.is_etf).toBe(true);
});
it("accepts and rejects smart-money thresholds at boundaries", () => {
const acceptedAsk = evaluateOptionSignal(
{
size: 100,
conditions: [],
underlying_id: "AAPL",
option_type: "call",
notional: 10_000,
nbbo_side: "A",
is_etf: false
},
baseConfig
);
expect(acceptedAsk.signalPass).toBe(true);
const rejectedLow = evaluateOptionSignal(
{
size: 100,
conditions: [],
underlying_id: "AAPL",
option_type: "call",
notional: 9_999,
nbbo_side: "A",
is_etf: false
},
baseConfig
);
expect(rejectedLow.signalPass).toBe(false);
const rejectedBid = evaluateOptionSignal(
{
size: 100,
conditions: [],
underlying_id: "AAPL",
option_type: "put",
notional: 24_999,
nbbo_side: "B",
is_etf: false
},
baseConfig
);
expect(rejectedBid.signalPass).toBe(false);
const acceptedSweep = evaluateOptionSignal(
{
size: 100,
conditions: ["SWEEP"],
underlying_id: "AAPL",
option_type: "call",
notional: 25_000,
nbbo_side: "MID",
is_etf: false
},
baseConfig
);
expect(acceptedSweep.signalPass).toBe(true);
const rejectedEtf = evaluateOptionSignal(
{
size: 100,
conditions: [],
underlying_id: "SPY",
option_type: "call",
notional: 49_999,
nbbo_side: "A",
is_etf: true
},
baseConfig
);
expect(rejectedEtf.signalPass).toBe(false);
});
});