927 lines
28 KiB
TypeScript
927 lines
28 KiB
TypeScript
import type { ClassifierHit, FlowPacket } from "@islandflow/types";
|
|
import { parseContractId, type ParsedContract } from "./contracts";
|
|
|
|
export type ClassifierConfig = {
|
|
sweepMinPremium: number;
|
|
sweepMinCount: number;
|
|
sweepMinPremiumZ: number;
|
|
spikeMinPremium: number;
|
|
spikeMinSize: number;
|
|
spikeMinPremiumZ: number;
|
|
spikeMinSizeZ: number;
|
|
zMinSamples: number;
|
|
minNbboCoverage: number;
|
|
minAggressorRatio: number;
|
|
zeroDteMaxAtmPct: number;
|
|
zeroDteMinPremium: number;
|
|
zeroDteMinSize: number;
|
|
};
|
|
|
|
const MS_PER_DAY = 86_400_000;
|
|
|
|
const clamp = (value: number, min = 0, max = 1): number => {
|
|
if (!Number.isFinite(value)) {
|
|
return min;
|
|
}
|
|
return Math.max(min, Math.min(max, value));
|
|
};
|
|
|
|
const formatUsd = (value: number): string => {
|
|
if (!Number.isFinite(value)) {
|
|
return "$0";
|
|
}
|
|
return `$${value.toFixed(2)}`;
|
|
};
|
|
|
|
const getNumberFeature = (packet: FlowPacket, key: string): number => {
|
|
const value = packet.features[key];
|
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
};
|
|
|
|
const getStringFeature = (packet: FlowPacket, key: string): string => {
|
|
const value = packet.features[key];
|
|
return typeof value === "string" ? value : "";
|
|
};
|
|
|
|
const formatPct = (value: number): string => `${Math.round(value * 100)}%`;
|
|
|
|
const formatPctPrecise = (value: number, digits = 2): string => {
|
|
if (!Number.isFinite(value)) {
|
|
return "0%";
|
|
}
|
|
return `${(value * 100).toFixed(digits)}%`;
|
|
};
|
|
|
|
const getAggressorContext = (
|
|
packet: FlowPacket
|
|
): {
|
|
coverage: number;
|
|
aggressiveBuyRatio: number;
|
|
aggressiveSellRatio: number;
|
|
aggressiveRatio: number;
|
|
} => {
|
|
return {
|
|
coverage: getNumberFeature(packet, "nbbo_coverage_ratio"),
|
|
aggressiveBuyRatio: getNumberFeature(packet, "nbbo_aggressive_buy_ratio"),
|
|
aggressiveSellRatio: getNumberFeature(packet, "nbbo_aggressive_sell_ratio"),
|
|
aggressiveRatio: getNumberFeature(packet, "nbbo_aggressive_ratio")
|
|
};
|
|
};
|
|
|
|
const applyAggressorAdjustment = (
|
|
confidence: number,
|
|
coverage: number,
|
|
aggressiveRatio: number,
|
|
config: ClassifierConfig
|
|
): { confidence: number; note: string } => {
|
|
if (!Number.isFinite(coverage) || coverage <= 0) {
|
|
return { confidence, note: "Aggressor mix unavailable (no NBBO coverage)." };
|
|
}
|
|
|
|
let adjusted = confidence;
|
|
if (coverage >= config.minNbboCoverage) {
|
|
if (aggressiveRatio >= config.minAggressorRatio) {
|
|
adjusted += 0.05;
|
|
} else {
|
|
adjusted -= 0.1;
|
|
}
|
|
}
|
|
|
|
const note = `Aggressor mix ${formatPct(aggressiveRatio)} aggressive, NBBO coverage ${formatPct(
|
|
coverage
|
|
)}.`;
|
|
|
|
return { confidence: adjusted, note };
|
|
};
|
|
|
|
type LargeActivity = {
|
|
count: number;
|
|
totalPremium: number;
|
|
totalSize: number;
|
|
windowMs: number;
|
|
premiumZ: number;
|
|
sizeZ: number;
|
|
premiumBaselineReady: boolean;
|
|
sizeBaselineReady: boolean;
|
|
passesAbsolute: boolean;
|
|
passesZ: boolean;
|
|
baselineNote: string;
|
|
};
|
|
|
|
const getLargeActivity = (packet: FlowPacket, config: ClassifierConfig): LargeActivity => {
|
|
const count = getNumberFeature(packet, "count");
|
|
const totalPremium = getNumberFeature(packet, "total_premium");
|
|
const totalSize = getNumberFeature(packet, "total_size");
|
|
const windowMs = getNumberFeature(packet, "window_ms");
|
|
const premiumZ = getNumberFeature(packet, "total_premium_z");
|
|
const sizeZ = getNumberFeature(packet, "total_size_z");
|
|
const premiumBaseline = getNumberFeature(packet, "total_premium_baseline_n");
|
|
const sizeBaseline = getNumberFeature(packet, "total_size_baseline_n");
|
|
|
|
const premiumBaselineReady = premiumBaseline >= config.zMinSamples;
|
|
const sizeBaselineReady = sizeBaseline >= config.zMinSamples;
|
|
const passesAbsolute = totalSize >= config.spikeMinSize && totalPremium >= config.spikeMinPremium;
|
|
const passesZ =
|
|
(premiumBaselineReady && premiumZ >= config.spikeMinPremiumZ) ||
|
|
(sizeBaselineReady && sizeZ >= config.spikeMinSizeZ);
|
|
|
|
const baselineNote =
|
|
premiumBaselineReady || sizeBaselineReady
|
|
? `Baseline z-scores: premium ${premiumZ.toFixed(2)}, size ${sizeZ.toFixed(2)}.`
|
|
: "Baseline z-scores unavailable.";
|
|
|
|
return {
|
|
count,
|
|
totalPremium,
|
|
totalSize,
|
|
windowMs,
|
|
premiumZ,
|
|
sizeZ,
|
|
premiumBaselineReady,
|
|
sizeBaselineReady,
|
|
passesAbsolute,
|
|
passesZ,
|
|
baselineNote
|
|
};
|
|
};
|
|
|
|
const applySideAggressorAdjustment = (
|
|
confidence: number,
|
|
coverage: number,
|
|
ratio: number,
|
|
config: ClassifierConfig,
|
|
label: string
|
|
): { confidence: number; note: string } => {
|
|
const normalizedCoverage = clamp(coverage, 0, 1);
|
|
const normalizedRatio = clamp(ratio, 0, 1);
|
|
let adjusted = confidence;
|
|
|
|
if (normalizedCoverage <= 0) {
|
|
return {
|
|
confidence: adjusted - 0.15,
|
|
note: "Aggressor mix unavailable (no NBBO coverage)."
|
|
};
|
|
}
|
|
|
|
if (normalizedCoverage < config.minNbboCoverage) {
|
|
adjusted -= 0.1;
|
|
}
|
|
|
|
if (normalizedRatio >= config.minAggressorRatio) {
|
|
adjusted += 0.05;
|
|
} else {
|
|
adjusted -= 0.1;
|
|
}
|
|
|
|
const note = `Aggressor mix ${formatPct(normalizedRatio)} ${label}, NBBO coverage ${formatPct(
|
|
normalizedCoverage
|
|
)}.`;
|
|
|
|
return { confidence: adjusted, note };
|
|
};
|
|
|
|
const getReferenceTs = (packet: FlowPacket): number | null => {
|
|
const endTs = getNumberFeature(packet, "end_ts");
|
|
if (endTs > 0) {
|
|
return endTs;
|
|
}
|
|
|
|
if (Number.isFinite(packet.source_ts) && packet.source_ts > 0) {
|
|
return packet.source_ts;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const getReferenceDay = (packet: FlowPacket): string | null => {
|
|
const referenceTs = getReferenceTs(packet);
|
|
if (!referenceTs) {
|
|
return null;
|
|
}
|
|
return new Date(referenceTs).toISOString().slice(0, 10);
|
|
};
|
|
|
|
const getDteDays = (packet: FlowPacket, contract: ParsedContract): number | null => {
|
|
const expiryTs = Date.parse(`${contract.expiry}T00:00:00Z`);
|
|
if (!Number.isFinite(expiryTs)) {
|
|
return null;
|
|
}
|
|
|
|
const referenceTs = getReferenceTs(packet);
|
|
if (!referenceTs) {
|
|
return null;
|
|
}
|
|
|
|
const diffMs = expiryTs - referenceTs;
|
|
if (diffMs < 0) {
|
|
return null;
|
|
}
|
|
|
|
return Math.ceil(diffMs / MS_PER_DAY);
|
|
};
|
|
|
|
const buildSweepHit = (
|
|
packet: FlowPacket,
|
|
contract: ParsedContract,
|
|
direction: "bullish" | "bearish",
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
const count = getNumberFeature(packet, "count");
|
|
const totalPremium = getNumberFeature(packet, "total_premium");
|
|
const totalSize = getNumberFeature(packet, "total_size");
|
|
const firstPrice = getNumberFeature(packet, "first_price");
|
|
const lastPrice = getNumberFeature(packet, "last_price");
|
|
const windowMs = getNumberFeature(packet, "window_ms");
|
|
const premiumZ = getNumberFeature(packet, "total_premium_z");
|
|
const premiumBaseline = getNumberFeature(packet, "total_premium_baseline_n");
|
|
const coverage = getNumberFeature(packet, "nbbo_coverage_ratio");
|
|
const aggressiveBuyRatio = getNumberFeature(packet, "nbbo_aggressive_buy_ratio");
|
|
const aggressiveSellRatio = getNumberFeature(packet, "nbbo_aggressive_sell_ratio");
|
|
const aggressiveRatio = Math.max(aggressiveBuyRatio, aggressiveSellRatio);
|
|
|
|
const baselineReady = premiumBaseline >= config.zMinSamples;
|
|
const passesAbsolute = totalPremium >= config.sweepMinPremium;
|
|
const passesZ = baselineReady && premiumZ >= config.sweepMinPremiumZ;
|
|
|
|
if (count < config.sweepMinCount || (!passesAbsolute && !passesZ)) {
|
|
return null;
|
|
}
|
|
|
|
const priceDelta = lastPrice - firstPrice;
|
|
const priceTrend = priceDelta >= 0 ? "up" : "down";
|
|
|
|
let confidence = 0.55;
|
|
if (priceDelta >= 0) {
|
|
confidence += 0.1;
|
|
}
|
|
if (count >= config.sweepMinCount + 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (totalPremium >= config.sweepMinPremium * 2) {
|
|
confidence += 0.15;
|
|
}
|
|
if (passesZ) {
|
|
confidence += 0.1;
|
|
if (premiumZ >= config.sweepMinPremiumZ + 1) {
|
|
confidence += 0.05;
|
|
}
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.95);
|
|
|
|
const baselineNote = baselineReady
|
|
? `Baseline premium z-score ${premiumZ.toFixed(2)} over ${Math.round(premiumBaseline)} samples.`
|
|
: "Baseline premium z-score unavailable.";
|
|
|
|
return {
|
|
classifier_id: direction === "bullish" ? "large_bullish_call_sweep" : "large_bearish_put_sweep",
|
|
confidence,
|
|
direction,
|
|
explanations: [
|
|
`Likely ${direction === "bullish" ? "call" : "put"} sweep: ${count} prints in ${Math.round(windowMs)}ms for ${packet.features.option_contract_id ?? packet.id}.`,
|
|
`Premium ${formatUsd(totalPremium)} across ${Math.round(totalSize)} contracts; price ${priceTrend}.`,
|
|
`Thresholds: >=${config.sweepMinCount} prints and >=${formatUsd(config.sweepMinPremium)} premium or z>=${config.sweepMinPremiumZ.toFixed(1)}.`,
|
|
baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildSpikeHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierHit | null => {
|
|
const activity = getLargeActivity(packet, config);
|
|
const { coverage, aggressiveBuyRatio, aggressiveSellRatio } = getAggressorContext(packet);
|
|
const aggressiveRatio = Math.max(aggressiveBuyRatio, aggressiveSellRatio);
|
|
|
|
if (!activity.passesAbsolute && !activity.passesZ) {
|
|
return null;
|
|
}
|
|
|
|
let confidence = 0.5;
|
|
if (activity.totalSize >= config.spikeMinSize * 2) {
|
|
confidence += 0.15;
|
|
}
|
|
if (activity.totalPremium >= config.spikeMinPremium * 2) {
|
|
confidence += 0.15;
|
|
}
|
|
if (activity.count >= 3) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.passesZ) {
|
|
confidence += 0.1;
|
|
if (
|
|
activity.premiumZ >= config.spikeMinPremiumZ + 1 ||
|
|
activity.sizeZ >= config.spikeMinSizeZ + 1
|
|
) {
|
|
confidence += 0.05;
|
|
}
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.9);
|
|
|
|
return {
|
|
classifier_id: "unusual_contract_spike",
|
|
confidence,
|
|
direction: "neutral",
|
|
explanations: [
|
|
`Unusual contract spike: ${activity.count} prints in ${Math.round(activity.windowMs)}ms for ${packet.features.option_contract_id ?? packet.id}.`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: >=${config.spikeMinSize} contracts and >=${formatUsd(config.spikeMinPremium)} premium or z>=${config.spikeMinPremiumZ.toFixed(1)}.`,
|
|
activity.baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildOverwriteHit = (
|
|
packet: FlowPacket,
|
|
contract: ParsedContract,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
if (contract.right !== "C") {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
if (!activity.passesAbsolute && !activity.passesZ) {
|
|
return null;
|
|
}
|
|
|
|
const { coverage, aggressiveSellRatio } = getAggressorContext(packet);
|
|
let confidence = 0.45;
|
|
if (activity.totalPremium >= config.spikeMinPremium * 2) {
|
|
confidence += 0.15;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.count >= 3) {
|
|
confidence += 0.05;
|
|
}
|
|
if (activity.passesZ) {
|
|
confidence += 0.1;
|
|
}
|
|
|
|
const aggressor = applySideAggressorAdjustment(
|
|
confidence,
|
|
coverage,
|
|
aggressiveSellRatio,
|
|
config,
|
|
"sell-side"
|
|
);
|
|
confidence = clamp(aggressor.confidence, 0, 0.9);
|
|
|
|
return {
|
|
classifier_id: "large_call_sell_overwrite",
|
|
confidence,
|
|
direction: "bearish",
|
|
explanations: [
|
|
`Likely call overwrite: ${activity.count} prints in ${Math.round(activity.windowMs)}ms for ${packet.features.option_contract_id ?? packet.id}.`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: >=${config.spikeMinSize} contracts and >=${formatUsd(config.spikeMinPremium)} premium or z>=${config.spikeMinPremiumZ.toFixed(1)}.`,
|
|
"Direction inferred from sell-side aggressor mix.",
|
|
activity.baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildPutWriteHit = (
|
|
packet: FlowPacket,
|
|
contract: ParsedContract,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
if (contract.right !== "P") {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
if (!activity.passesAbsolute && !activity.passesZ) {
|
|
return null;
|
|
}
|
|
|
|
const { coverage, aggressiveSellRatio } = getAggressorContext(packet);
|
|
let confidence = 0.45;
|
|
if (activity.totalPremium >= config.spikeMinPremium * 2) {
|
|
confidence += 0.15;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.count >= 3) {
|
|
confidence += 0.05;
|
|
}
|
|
if (activity.passesZ) {
|
|
confidence += 0.1;
|
|
}
|
|
|
|
const aggressor = applySideAggressorAdjustment(
|
|
confidence,
|
|
coverage,
|
|
aggressiveSellRatio,
|
|
config,
|
|
"sell-side"
|
|
);
|
|
confidence = clamp(aggressor.confidence, 0, 0.9);
|
|
|
|
return {
|
|
classifier_id: "large_put_sell_write",
|
|
confidence,
|
|
direction: "bullish",
|
|
explanations: [
|
|
`Likely put write: ${activity.count} prints in ${Math.round(activity.windowMs)}ms for ${packet.features.option_contract_id ?? packet.id}.`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: >=${config.spikeMinSize} contracts and >=${formatUsd(config.spikeMinPremium)} premium or z>=${config.spikeMinPremiumZ.toFixed(1)}.`,
|
|
"Direction inferred from sell-side aggressor mix.",
|
|
activity.baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildStraddleStrangleHit = (
|
|
packet: FlowPacket,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
const structureType = getStringFeature(packet, "structure_type");
|
|
if (structureType !== "straddle" && structureType !== "strangle") {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
const { coverage, aggressiveBuyRatio, aggressiveSellRatio, aggressiveRatio } =
|
|
getAggressorContext(packet);
|
|
const structureLegs = getNumberFeature(packet, "structure_legs");
|
|
const structureStrikes = getNumberFeature(packet, "structure_strikes");
|
|
const strikeSpan = getNumberFeature(packet, "structure_strike_span");
|
|
|
|
let confidence = 0.45;
|
|
if (activity.totalPremium >= config.spikeMinPremium) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize) {
|
|
confidence += 0.05;
|
|
}
|
|
if (structureLegs >= 4) {
|
|
confidence += 0.05;
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.85);
|
|
|
|
let volBias = "mixed aggressor skew";
|
|
if (aggressiveBuyRatio >= aggressiveSellRatio + 0.1) {
|
|
volBias = "buy-side skew suggests long volatility";
|
|
} else if (aggressiveSellRatio >= aggressiveBuyRatio + 0.1) {
|
|
volBias = "sell-side skew suggests short volatility";
|
|
}
|
|
|
|
const skewNote = `Aggressor skew: buy ${formatPct(aggressiveBuyRatio)}, sell ${formatPct(
|
|
aggressiveSellRatio
|
|
)}; ${volBias}.`;
|
|
|
|
return {
|
|
classifier_id: structureType === "straddle" ? "straddle" : "strangle",
|
|
confidence,
|
|
direction: "neutral",
|
|
explanations: [
|
|
`Likely ${structureType}: ${structureLegs} legs across ${structureStrikes} strikes (span ${strikeSpan}).`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
skewNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildVerticalSpreadHit = (
|
|
packet: FlowPacket,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
const structureType = getStringFeature(packet, "structure_type");
|
|
if (structureType !== "vertical") {
|
|
return null;
|
|
}
|
|
|
|
const structureRights = getStringFeature(packet, "structure_rights");
|
|
if (structureRights !== "C" && structureRights !== "P") {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
const { coverage, aggressiveBuyRatio, aggressiveSellRatio } = getAggressorContext(packet);
|
|
const structureLegs = getNumberFeature(packet, "structure_legs");
|
|
const structureStrikes = getNumberFeature(packet, "structure_strikes");
|
|
const strikeSpan = getNumberFeature(packet, "structure_strike_span");
|
|
|
|
let confidence = 0.5;
|
|
if (activity.totalPremium >= config.spikeMinPremium) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize) {
|
|
confidence += 0.05;
|
|
}
|
|
if (structureLegs >= 3) {
|
|
confidence += 0.05;
|
|
}
|
|
|
|
let direction: "bullish" | "bearish" | "neutral" = "neutral";
|
|
let biasNote = "Debit/credit bias unclear (insufficient aggressor data).";
|
|
let aggressorNote = "Aggressor mix unavailable (no NBBO coverage).";
|
|
const hasAggressor = coverage > 0 && aggressiveBuyRatio + aggressiveSellRatio > 0;
|
|
if (hasAggressor) {
|
|
const buyDominant = aggressiveBuyRatio >= aggressiveSellRatio;
|
|
const dominantRatio = buyDominant ? aggressiveBuyRatio : aggressiveSellRatio;
|
|
const label = buyDominant ? "buy-side" : "sell-side";
|
|
const aggressor = applySideAggressorAdjustment(
|
|
confidence,
|
|
coverage,
|
|
dominantRatio,
|
|
config,
|
|
label
|
|
);
|
|
confidence = aggressor.confidence;
|
|
aggressorNote = aggressor.note;
|
|
|
|
const spreadBias = buyDominant ? "debit" : "credit";
|
|
biasNote = `Aggressor skew: buy ${formatPct(aggressiveBuyRatio)}, sell ${formatPct(
|
|
aggressiveSellRatio
|
|
)}; suggests ${spreadBias} ${structureRights === "C" ? "call" : "put"} vertical.`;
|
|
|
|
if (structureRights === "C") {
|
|
direction = buyDominant ? "bullish" : "bearish";
|
|
} else {
|
|
direction = buyDominant ? "bearish" : "bullish";
|
|
}
|
|
} else {
|
|
confidence -= 0.1;
|
|
}
|
|
|
|
confidence = clamp(confidence, 0, 0.85);
|
|
|
|
return {
|
|
classifier_id: "vertical_spread",
|
|
confidence,
|
|
direction,
|
|
explanations: [
|
|
`Likely vertical spread: ${structureLegs} legs across ${structureStrikes} strikes (span ${strikeSpan}).`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
biasNote,
|
|
aggressorNote,
|
|
"Direction inferred from debit/credit bias."
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildLadderHit = (
|
|
packet: FlowPacket,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
const structureType = getStringFeature(packet, "structure_type");
|
|
if (structureType !== "ladder") {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
const { coverage, aggressiveRatio } = getAggressorContext(packet);
|
|
const structureRights = getStringFeature(packet, "structure_rights");
|
|
const structureLegs = getNumberFeature(packet, "structure_legs");
|
|
const structureStrikes = getNumberFeature(packet, "structure_strikes");
|
|
const strikeSpan = getNumberFeature(packet, "structure_strike_span");
|
|
|
|
const qualifies =
|
|
activity.totalPremium >= config.spikeMinPremium ||
|
|
activity.totalSize >= config.spikeMinSize ||
|
|
activity.passesZ;
|
|
if (!qualifies) {
|
|
return null;
|
|
}
|
|
|
|
let confidence = 0.45;
|
|
if (activity.totalPremium >= config.spikeMinPremium * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (structureStrikes >= 4) {
|
|
confidence += 0.05;
|
|
}
|
|
if (activity.passesZ) {
|
|
confidence += 0.05;
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.85);
|
|
|
|
let direction: "bullish" | "bearish" | "neutral" = "neutral";
|
|
if (structureRights === "C") {
|
|
direction = "bullish";
|
|
} else if (structureRights === "P") {
|
|
direction = "bearish";
|
|
}
|
|
|
|
return {
|
|
classifier_id: "ladder_accumulation",
|
|
confidence,
|
|
direction,
|
|
explanations: [
|
|
`Likely multi-strike ladder accumulation: ${structureLegs} legs across ${structureStrikes} strikes (span ${strikeSpan}).`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: ladder structure plus >=${config.spikeMinSize} contracts or >=${formatUsd(config.spikeMinPremium)} premium.`,
|
|
"Direction inferred from call/put ladder.",
|
|
activity.baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildRollHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierHit | null => {
|
|
const structureType = getStringFeature(packet, "structure_type");
|
|
if (structureType !== "roll") {
|
|
return null;
|
|
}
|
|
|
|
const structureRights = getStringFeature(packet, "structure_rights");
|
|
if (structureRights !== "C" && structureRights !== "P") {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
const qualifies = activity.totalPremium >= config.spikeMinPremium || activity.totalSize >= config.spikeMinSize;
|
|
if (!qualifies) {
|
|
return null;
|
|
}
|
|
|
|
const { coverage, aggressiveBuyRatio, aggressiveSellRatio, aggressiveRatio } =
|
|
getAggressorContext(packet);
|
|
const fromExpiry = getStringFeature(packet, "roll_from_expiry") || "";
|
|
const toExpiry = getStringFeature(packet, "roll_to_expiry") || "";
|
|
|
|
const getOptionalNumber = (key: string): number | null => {
|
|
const value = packet.features[key];
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
};
|
|
|
|
const fromStrike = getOptionalNumber("roll_from_strike");
|
|
const toStrike = getOptionalNumber("roll_to_strike");
|
|
const strikeDelta = getOptionalNumber("roll_strike_delta") ?? 0;
|
|
const expiryDaysDelta = getOptionalNumber("roll_expiry_days_delta");
|
|
|
|
const hasStrikePair = fromStrike !== null && toStrike !== null;
|
|
const hasExpiryPair = Boolean(fromExpiry) && Boolean(toExpiry);
|
|
|
|
let rollFlavor = "roll out";
|
|
if (hasStrikePair) {
|
|
if (strikeDelta > 0.0001) {
|
|
rollFlavor = "roll out and up";
|
|
} else if (strikeDelta < -0.0001) {
|
|
rollFlavor = "roll out and down";
|
|
}
|
|
}
|
|
|
|
let direction: "bullish" | "bearish" | "neutral" = "neutral";
|
|
if (hasStrikePair) {
|
|
if (structureRights === "C") {
|
|
direction = strikeDelta > 0.0001 ? "bullish" : strikeDelta < -0.0001 ? "bearish" : "neutral";
|
|
} else {
|
|
direction = strikeDelta > 0.0001 ? "bearish" : strikeDelta < -0.0001 ? "bullish" : "neutral";
|
|
}
|
|
}
|
|
|
|
let confidence = 0.5;
|
|
if (activity.totalPremium >= config.spikeMinPremium * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize * 2) {
|
|
confidence += 0.05;
|
|
}
|
|
if (hasStrikePair && Math.abs(strikeDelta) > 0.0001) {
|
|
confidence += 0.05;
|
|
}
|
|
if (hasExpiryPair && expiryDaysDelta !== null && expiryDaysDelta >= 7) {
|
|
confidence += 0.05;
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.85);
|
|
|
|
const expiryNote = hasExpiryPair
|
|
? `Expiries: ${fromExpiry} -> ${toExpiry}${
|
|
expiryDaysDelta !== null && expiryDaysDelta !== 0 ? ` (${Math.round(expiryDaysDelta)}d)` : ""
|
|
}.`
|
|
: "Expiry pairing unavailable.";
|
|
const strikeNote = hasStrikePair
|
|
? `Strikes: ${fromStrike} -> ${toStrike} (delta ${strikeDelta}).`
|
|
: "Strike pairing unavailable.";
|
|
const skewNote = `Aggressor skew: buy ${formatPct(aggressiveBuyRatio)}, sell ${formatPct(
|
|
aggressiveSellRatio
|
|
)}.`;
|
|
|
|
return {
|
|
classifier_id: "roll_up_down_out",
|
|
confidence,
|
|
direction,
|
|
explanations: [
|
|
`Consistent with ${rollFlavor}: ${activity.count} prints in ${Math.round(activity.windowMs)}ms for ${packet.features.underlying_id ?? packet.id}.`,
|
|
expiryNote,
|
|
strikeNote,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: >=${config.spikeMinSize} contracts or >=${formatUsd(config.spikeMinPremium)} premium.`,
|
|
skewNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildFarDatedHit = (
|
|
packet: FlowPacket,
|
|
contract: ParsedContract,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
const dteDays = getDteDays(packet, contract);
|
|
if (!dteDays || dteDays < 60) {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
if (!activity.passesAbsolute && !activity.passesZ) {
|
|
return null;
|
|
}
|
|
|
|
const { coverage, aggressiveRatio } = getAggressorContext(packet);
|
|
let confidence = 0.5;
|
|
if (dteDays >= 90) {
|
|
confidence += 0.05;
|
|
}
|
|
if (activity.totalPremium >= config.spikeMinPremium * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.totalSize >= config.spikeMinSize * 2) {
|
|
confidence += 0.05;
|
|
}
|
|
if (activity.passesZ) {
|
|
confidence += 0.1;
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.85);
|
|
|
|
return {
|
|
classifier_id: "far_dated_conviction",
|
|
confidence,
|
|
direction: contract.right === "C" ? "bullish" : "bearish",
|
|
explanations: [
|
|
`Likely far-dated ${contract.right === "C" ? "call" : "put"} positioning: ${dteDays} DTE for ${packet.features.option_contract_id ?? packet.id}.`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: DTE >=60 and >=${config.spikeMinSize} contracts or >=${formatUsd(config.spikeMinPremium)} premium (or z-scores).`,
|
|
"Direction inferred from call/put right.",
|
|
activity.baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
const buildZeroDteGammaPunchHit = (
|
|
packet: FlowPacket,
|
|
contract: ParsedContract,
|
|
config: ClassifierConfig
|
|
): ClassifierHit | null => {
|
|
const referenceDay = getReferenceDay(packet);
|
|
if (!referenceDay || contract.expiry !== referenceDay) {
|
|
return null;
|
|
}
|
|
|
|
const activity = getLargeActivity(packet, config);
|
|
if (
|
|
activity.totalPremium < config.zeroDteMinPremium ||
|
|
activity.totalSize < config.zeroDteMinSize
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const underlyingMid = getNumberFeature(packet, "underlying_mid");
|
|
if (!Number.isFinite(underlyingMid) || underlyingMid <= 0) {
|
|
return null;
|
|
}
|
|
|
|
const strike = contract.strike;
|
|
const atmPct = Math.abs(strike - underlyingMid) / underlyingMid;
|
|
if (atmPct > config.zeroDteMaxAtmPct) {
|
|
return null;
|
|
}
|
|
|
|
const { coverage, aggressiveRatio } = getAggressorContext(packet);
|
|
let confidence = 0.55;
|
|
if (atmPct <= config.zeroDteMaxAtmPct * 0.5) {
|
|
confidence += 0.05;
|
|
}
|
|
if (activity.totalPremium >= config.zeroDteMinPremium * 2) {
|
|
confidence += 0.1;
|
|
}
|
|
if (activity.totalSize >= config.zeroDteMinSize * 2) {
|
|
confidence += 0.05;
|
|
}
|
|
|
|
const aggressor = applyAggressorAdjustment(confidence, coverage, aggressiveRatio, config);
|
|
confidence = clamp(aggressor.confidence, 0, 0.9);
|
|
|
|
return {
|
|
classifier_id: "zero_dte_gamma_punch",
|
|
confidence,
|
|
direction: contract.right === "C" ? "bullish" : "bearish",
|
|
explanations: [
|
|
`Likely 0DTE gamma punch: ${packet.features.option_contract_id ?? packet.id} near ATM.`,
|
|
`Underlying mid ${formatUsd(underlyingMid)}, strike ${formatUsd(strike)} (${formatPctPrecise(atmPct)} from ATM).`,
|
|
`Premium ${formatUsd(activity.totalPremium)} across ${Math.round(activity.totalSize)} contracts.`,
|
|
`Thresholds: DTE=0, ATM <=${formatPctPrecise(config.zeroDteMaxAtmPct)}, >=${formatUsd(
|
|
config.zeroDteMinPremium
|
|
)} premium, >=${config.zeroDteMinSize} contracts.`,
|
|
activity.baselineNote,
|
|
aggressor.note
|
|
]
|
|
};
|
|
};
|
|
|
|
export const evaluateClassifiers = (
|
|
packet: FlowPacket,
|
|
config: ClassifierConfig
|
|
): ClassifierHit[] => {
|
|
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 contract = structureOnly ? null : parseContractId(contractId);
|
|
const hits: ClassifierHit[] = [];
|
|
|
|
if (structureOnly) {
|
|
const structureHit = buildStraddleStrangleHit(packet, config);
|
|
if (structureHit) {
|
|
hits.push(structureHit);
|
|
}
|
|
|
|
const verticalHit = buildVerticalSpreadHit(packet, config);
|
|
if (verticalHit) {
|
|
hits.push(verticalHit);
|
|
}
|
|
|
|
const ladderHit = buildLadderHit(packet, config);
|
|
if (ladderHit) {
|
|
hits.push(ladderHit);
|
|
}
|
|
|
|
const rollHit = buildRollHit(packet, config);
|
|
if (rollHit) {
|
|
hits.push(rollHit);
|
|
}
|
|
|
|
return hits;
|
|
}
|
|
|
|
if (!structureOnly) {
|
|
if (contract?.right === "C") {
|
|
const hit = buildSweepHit(packet, contract, "bullish", config);
|
|
if (hit) {
|
|
hits.push(hit);
|
|
}
|
|
}
|
|
|
|
if (contract?.right === "P") {
|
|
const hit = buildSweepHit(packet, contract, "bearish", config);
|
|
if (hit) {
|
|
hits.push(hit);
|
|
}
|
|
}
|
|
|
|
const spikeHit = buildSpikeHit(packet, config);
|
|
if (spikeHit) {
|
|
hits.push(spikeHit);
|
|
}
|
|
|
|
if (contract) {
|
|
const overwriteHit = buildOverwriteHit(packet, contract, config);
|
|
if (overwriteHit) {
|
|
hits.push(overwriteHit);
|
|
}
|
|
|
|
const putWriteHit = buildPutWriteHit(packet, contract, config);
|
|
if (putWriteHit) {
|
|
hits.push(putWriteHit);
|
|
}
|
|
|
|
const farDatedHit = buildFarDatedHit(packet, contract, config);
|
|
if (farDatedHit) {
|
|
hits.push(farDatedHit);
|
|
}
|
|
|
|
const zeroDteHit = buildZeroDteGammaPunchHit(packet, contract, config);
|
|
if (zeroDteHit) {
|
|
hits.push(zeroDteHit);
|
|
}
|
|
}
|
|
}
|
|
|
|
return hits;
|
|
};
|