Join underlying quotes for 0DTE classifier
This commit is contained in:
parent
f96f5699ef
commit
2752025fbc
5 changed files with 145 additions and 3 deletions
|
|
@ -12,6 +12,9 @@ export type ClassifierConfig = {
|
|||
zMinSamples: number;
|
||||
minNbboCoverage: number;
|
||||
minAggressorRatio: number;
|
||||
zeroDteMaxAtmPct: number;
|
||||
zeroDteMinPremium: number;
|
||||
zeroDteMinSize: number;
|
||||
};
|
||||
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
|
|
@ -42,6 +45,13 @@ const getStringFeature = (packet: FlowPacket, key: string): string => {
|
|||
|
||||
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
|
||||
): {
|
||||
|
|
@ -183,6 +193,14 @@ const getReferenceTs = (packet: FlowPacket): number | null => {
|
|||
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)) {
|
||||
|
|
@ -666,6 +684,67 @@ const buildFarDatedHit = (
|
|||
};
|
||||
};
|
||||
|
||||
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
|
||||
|
|
@ -710,6 +789,11 @@ export const evaluateClassifiers = (
|
|||
if (farDatedHit) {
|
||||
hits.push(farDatedHit);
|
||||
}
|
||||
|
||||
const zeroDteHit = buildZeroDteGammaPunchHit(packet, contract, config);
|
||||
if (zeroDteHit) {
|
||||
hits.push(zeroDteHit);
|
||||
}
|
||||
}
|
||||
|
||||
const structureHit = buildStraddleStrangleHit(packet, config);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,10 @@ const envSchema = z.object({
|
|||
CLASSIFIER_SPIKE_MIN_SIZE_Z: z.coerce.number().nonnegative().default(2),
|
||||
CLASSIFIER_Z_MIN_SAMPLES: z.coerce.number().int().nonnegative().default(12),
|
||||
CLASSIFIER_MIN_NBBO_COVERAGE: z.coerce.number().min(0).max(1).default(0.5),
|
||||
CLASSIFIER_MIN_AGGRESSOR_RATIO: z.coerce.number().min(0).max(1).default(0.55)
|
||||
CLASSIFIER_MIN_AGGRESSOR_RATIO: z.coerce.number().min(0).max(1).default(0.55),
|
||||
CLASSIFIER_0DTE_MAX_ATM_PCT: z.coerce.number().min(0).max(1).default(0.01),
|
||||
CLASSIFIER_0DTE_MIN_PREMIUM: z.coerce.number().positive().default(20_000),
|
||||
CLASSIFIER_0DTE_MIN_SIZE: z.coerce.number().int().positive().default(400)
|
||||
});
|
||||
|
||||
const env = readEnv(envSchema);
|
||||
|
|
@ -130,7 +133,10 @@ const classifierConfig: ClassifierConfig = {
|
|||
spikeMinSizeZ: env.CLASSIFIER_SPIKE_MIN_SIZE_Z,
|
||||
zMinSamples: env.CLASSIFIER_Z_MIN_SAMPLES,
|
||||
minNbboCoverage: env.CLASSIFIER_MIN_NBBO_COVERAGE,
|
||||
minAggressorRatio: env.CLASSIFIER_MIN_AGGRESSOR_RATIO
|
||||
minAggressorRatio: env.CLASSIFIER_MIN_AGGRESSOR_RATIO,
|
||||
zeroDteMaxAtmPct: env.CLASSIFIER_0DTE_MAX_ATM_PCT,
|
||||
zeroDteMinPremium: env.CLASSIFIER_0DTE_MIN_PREMIUM,
|
||||
zeroDteMinSize: env.CLASSIFIER_0DTE_MIN_SIZE
|
||||
};
|
||||
|
||||
const darkInferenceConfig: DarkInferenceConfig = {
|
||||
|
|
@ -485,6 +491,34 @@ const flushCluster = async (
|
|||
window_ms: env.CLUSTER_WINDOW_MS
|
||||
};
|
||||
|
||||
const parsedContract = parseContractId(cluster.contractId);
|
||||
if (parsedContract?.root) {
|
||||
features.underlying_id = parsedContract.root;
|
||||
const quoteJoin = selectEquityQuote(parsedContract.root, cluster.endTs);
|
||||
if (!quoteJoin.quote) {
|
||||
joinQuality.underlying_quote_missing = 1;
|
||||
} else {
|
||||
joinQuality.underlying_quote_age_ms = quoteJoin.ageMs;
|
||||
if (quoteJoin.stale) {
|
||||
joinQuality.underlying_quote_stale = 1;
|
||||
} else {
|
||||
const bid = quoteJoin.quote.bid;
|
||||
const ask = quoteJoin.quote.ask;
|
||||
if (Number.isFinite(bid) && Number.isFinite(ask) && ask > 0) {
|
||||
const mid = (bid + ask) / 2;
|
||||
const spread = ask - bid;
|
||||
features.underlying_quote_ts = quoteJoin.quote.ts;
|
||||
features.underlying_bid = bid;
|
||||
features.underlying_ask = ask;
|
||||
features.underlying_mid = roundTo(mid);
|
||||
features.underlying_spread = roundTo(spread);
|
||||
} else {
|
||||
joinQuality.underlying_quote_missing = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const placementTotal =
|
||||
cluster.placements.aa +
|
||||
cluster.placements.a +
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ const baseConfig: ClassifierConfig = {
|
|||
spikeMinSizeZ: 2,
|
||||
zMinSamples: 12,
|
||||
minNbboCoverage: 0.5,
|
||||
minAggressorRatio: 0.55
|
||||
minAggressorRatio: 0.55,
|
||||
zeroDteMaxAtmPct: 0.01,
|
||||
zeroDteMinPremium: 20_000,
|
||||
zeroDteMinSize: 400
|
||||
};
|
||||
|
||||
const DEFAULT_TS = Date.UTC(2024, 0, 2);
|
||||
|
|
@ -187,4 +190,19 @@ describe("classifier structure and positioning signals", () => {
|
|||
const hit = hits.find((candidate) => candidate.classifier_id === "far_dated_conviction");
|
||||
expect(hit?.direction).toBe("bullish");
|
||||
});
|
||||
|
||||
test("zero dte gamma punch triggers when ATM and large", () => {
|
||||
const packet = buildPacket({
|
||||
option_contract_id: "SPY-2024-01-02-450-C",
|
||||
total_premium: 35_000,
|
||||
total_size: 600,
|
||||
underlying_mid: 450,
|
||||
nbbo_coverage_ratio: 0.8,
|
||||
nbbo_aggressive_buy_ratio: 0.7,
|
||||
nbbo_aggressive_sell_ratio: 0.3
|
||||
});
|
||||
const hits = evaluateClassifiers(packet, baseConfig);
|
||||
const hit = hits.find((candidate) => candidate.classifier_id === "zero_dte_gamma_punch");
|
||||
expect(hit?.direction).toBe("bullish");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue