gate option print clickhouse persistence by signal pass

This commit is contained in:
dirtydishes 2026-05-22 23:06:27 -04:00
parent 828c81bcc6
commit 955eccce3c
7 changed files with 352 additions and 5 deletions

View file

@ -44,6 +44,7 @@ import { createIbkrOptionsAdapter } from "./adapters/ibkr";
import { createSyntheticOptionsAdapter } from "./adapters/synthetic";
import type { OptionIngestAdapter, StopHandler } from "./adapters/types";
import { enrichOptionPrint, rememberContext, selectAtOrBefore, type ContextHistory } from "./enrichment";
import { processOptionTrade } from "./trade-pipeline";
import { z } from "zod";
const service = "ingest-options";
@ -104,6 +105,20 @@ const envSchema = z.object({
OPTIONS_SIGNAL_ETF_UNDERLYINGS: z
.string()
.default("SPY,QQQ,IWM,DIA,TLT,GLD,SLV,XLF,XLE,XLV,XLI,XLP,XLU,XLY,SMH,ARKK"),
OPTIONS_PERSIST_SIGNAL_ONLY: z
.preprocess((value) => {
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
}
}
return value;
}, z.boolean())
.default(true),
TESTING_MODE: z
.preprocess((value) => {
if (typeof value === "string") {
@ -400,6 +415,9 @@ const run = async () => {
() => syntheticControl
);
logger.info("ingest adapter selected", { adapter: adapter.name });
logger.info("option print clickhouse persistence mode", {
signal_only: env.OPTIONS_PERSIST_SIGNAL_ONLY
});
const allowPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS);
const allowNbboPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS);
@ -426,11 +444,12 @@ const run = async () => {
const print = enrichOptionPrint(rawPrint, optionQuote, equityQuote, optionsSignalConfig);
try {
await insertOptionPrint(clickhouse, print);
await publishJson(js, SUBJECT_OPTION_PRINTS, print);
if (print.signal_pass) {
await publishJson(js, SUBJECT_OPTION_SIGNAL_PRINTS, print);
}
await processOptionTrade(print, {
persistSignalOnly: env.OPTIONS_PERSIST_SIGNAL_ONLY,
persist: async (value) => insertOptionPrint(clickhouse, value),
publishRaw: async (value) => publishJson(js, SUBJECT_OPTION_PRINTS, value),
publishSignal: async (value) => publishJson(js, SUBJECT_OPTION_SIGNAL_PRINTS, value)
});
} catch (error) {
if (isExpectedShutdownError(error)) {
return;

View file

@ -0,0 +1,24 @@
import type { OptionPrint } from "@islandflow/types";
export type ProcessOptionTradeDeps = {
persistSignalOnly: boolean;
persist: (print: OptionPrint) => Promise<void>;
publishRaw: (print: OptionPrint) => Promise<void>;
publishSignal: (print: OptionPrint) => Promise<void>;
};
export const shouldPersistOptionPrint = (print: Pick<OptionPrint, "signal_pass">, persistSignalOnly: boolean): boolean => {
return !persistSignalOnly || print.signal_pass === true;
};
export const processOptionTrade = async (print: OptionPrint, deps: ProcessOptionTradeDeps): Promise<void> => {
if (shouldPersistOptionPrint(print, deps.persistSignalOnly)) {
await deps.persist(print);
}
await deps.publishRaw(print);
if (print.signal_pass) {
await deps.publishSignal(print);
}
};

View file

@ -0,0 +1,101 @@
import { describe, expect, it } from "bun:test";
import type { OptionPrint } from "@islandflow/types";
import { processOptionTrade, shouldPersistOptionPrint } from "../src/trade-pipeline";
const makePrint = (signalPass: boolean): OptionPrint => ({
source_ts: 1_000,
ingest_ts: 1_001,
seq: 1,
trace_id: `print-${signalPass ? "pass" : "fail"}`,
ts: 1_000,
option_contract_id: "SPY-2025-01-17-450-C",
price: 1.25,
size: 100,
exchange: "TEST",
signal_pass: signalPass
});
describe("option trade persistence gating", () => {
it("does not persist failing prints when signal-only persistence is enabled", async () => {
const persisted: string[] = [];
const rawPublished: string[] = [];
const signalPublished: string[] = [];
await processOptionTrade(makePrint(false), {
persistSignalOnly: true,
persist: async (print) => {
persisted.push(print.trace_id);
},
publishRaw: async (print) => {
rawPublished.push(print.trace_id);
},
publishSignal: async (print) => {
signalPublished.push(print.trace_id);
}
});
expect(persisted).toEqual([]);
expect(rawPublished).toEqual(["print-fail"]);
expect(signalPublished).toEqual([]);
});
it("persists and publishes passing prints when signal-only persistence is enabled", async () => {
const persisted: string[] = [];
const rawPublished: string[] = [];
const signalPublished: string[] = [];
await processOptionTrade(makePrint(true), {
persistSignalOnly: true,
persist: async (print) => {
persisted.push(print.trace_id);
},
publishRaw: async (print) => {
rawPublished.push(print.trace_id);
},
publishSignal: async (print) => {
signalPublished.push(print.trace_id);
}
});
expect(persisted).toEqual(["print-pass"]);
expect(rawPublished).toEqual(["print-pass"]);
expect(signalPublished).toEqual(["print-pass"]);
});
it("persists failing prints when signal-only persistence is disabled", async () => {
const persisted: string[] = [];
const rawPublished: string[] = [];
const signalPublished: string[] = [];
await processOptionTrade(makePrint(false), {
persistSignalOnly: false,
persist: async (print) => {
persisted.push(print.trace_id);
},
publishRaw: async (print) => {
rawPublished.push(print.trace_id);
},
publishSignal: async (print) => {
signalPublished.push(print.trace_id);
}
});
expect(persisted).toEqual(["print-fail"]);
expect(rawPublished).toEqual(["print-fail"]);
expect(signalPublished).toEqual([]);
});
});
describe("shouldPersistOptionPrint", () => {
it("returns true for passing prints in signal-only mode", () => {
expect(shouldPersistOptionPrint({ signal_pass: true }, true)).toBe(true);
});
it("returns false for failing prints in signal-only mode", () => {
expect(shouldPersistOptionPrint({ signal_pass: false }, true)).toBe(false);
});
it("returns true for failing prints when signal-only mode is disabled", () => {
expect(shouldPersistOptionPrint({ signal_pass: false }, false)).toBe(true);
});
});