Add smart money event calendar enrichment

This commit is contained in:
dirtydishes 2026-05-04 19:21:18 -04:00
parent 6108aea166
commit 6b794ec7ac
11 changed files with 270 additions and 8 deletions

View file

@ -1,5 +1,10 @@
import { readEnv } from "@islandflow/config";
import { createLogger } from "@islandflow/observability";
import {
createEmptyEventCalendarProvider,
loadEventCalendarProviderFromFile,
type EventCalendarProvider
} from "@islandflow/refdata/event-calendar";
import {
SUBJECT_ALERTS,
SUBJECT_CLASSIFIER_HITS,
@ -135,10 +140,12 @@ const envSchema = z.object({
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)
CLASSIFIER_0DTE_MIN_SIZE: z.coerce.number().int().positive().default(400),
SMART_MONEY_EVENT_CALENDAR_PATH: z.string().optional()
});
const env = readEnv(envSchema);
let eventCalendarProvider: EventCalendarProvider = createEmptyEventCalendarProvider();
const classifierConfig: ClassifierConfig = {
sweepMinPremium: env.CLASSIFIER_SWEEP_MIN_PREMIUM,
@ -898,7 +905,16 @@ const emitClassifiers = async (
): Promise<void> => {
let smartMoneyEvent: SmartMoneyEvent;
try {
smartMoneyEvent = SmartMoneyEventSchema.parse(buildSmartMoneyEventFromPacket(packet));
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;
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 }));
await insertSmartMoneyEvent(clickhouse, smartMoneyEvent);
await publishJson(js, SUBJECT_SMART_MONEY_EVENTS, smartMoneyEvent);
} catch (error) {
@ -1200,6 +1216,19 @@ const run = async () => {
database: env.CLICKHOUSE_DATABASE
});
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 });
} 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)
});
}
}
const redis = createRedisClient(env.REDIS_URL);
redis.on("error", (error) => {
logger.warn("redis client error", { error: error instanceof Error ? error.message : String(error) });

View file

@ -8,6 +8,7 @@ import {
type SmartMoneyProfileId,
type SmartMoneyProfileScore
} from "@islandflow/types";
import type { EventCalendarMatch } from "@islandflow/refdata/event-calendar";
import { parseContractId } from "./contracts";
const MS_PER_DAY = 86_400_000;
@ -97,7 +98,11 @@ const inferDirection = (packet: FlowPacket): SmartMoneyDirection => {
return "neutral";
};
const buildFeatures = (packet: FlowPacket): SmartMoneyFeatures => {
export type SmartMoneyParentEventOptions = {
eventCalendarMatch?: EventCalendarMatch | null;
};
const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions = {}): SmartMoneyFeatures => {
const contractId = stringFeature(packet, "option_contract_id");
const contract = parseContractId(contractId);
const underlyingMid = numberFeature(packet, "underlying_mid");
@ -108,7 +113,8 @@ const buildFeatures = (packet: FlowPacket): SmartMoneyFeatures => {
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 specialCount = numberFeature(packet, "special_print_count");
const eventTs = numberFeature(packet, "corporate_event_ts");
const calendarEventTs = options.eventCalendarMatch?.event_ts ?? null;
const eventTs = calendarEventTs ?? numberFeature(packet, "corporate_event_ts");
const referenceTs = getReferenceTs(packet);
const expiryTs = contract ? Date.parse(`${contract.expiry}T00:00:00Z`) : Number.NaN;
@ -259,8 +265,11 @@ const evaluateProfiles = (
return scores.sort((a, b) => b.probability - a.probability);
};
export const buildSmartMoneyEventFromPacket = (packet: FlowPacket): SmartMoneyEvent => {
const features = buildFeatures(packet);
export const buildSmartMoneyEventFromPacket = (
packet: FlowPacket,
options: SmartMoneyParentEventOptions = {}
): SmartMoneyEvent => {
const features = buildFeatures(packet, options);
const suppressed = detectSuppression(packet, features);
const profileScores = evaluateProfiles(packet, features, suppressed);
const primary = profileScores[0] ?? null;