Add smart money event calendar enrichment
This commit is contained in:
parent
6108aea166
commit
6b794ec7ac
11 changed files with 270 additions and 8 deletions
|
|
@ -9,6 +9,7 @@
|
|||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/refdata": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"redis": "^5.10.0",
|
||||
|
|
|
|||
|
|
@ -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) });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -55,4 +55,58 @@ describe("smart money parent events", () => {
|
|||
expect(event.primary_profile_id).toBeNull();
|
||||
expect(event.suppressed_reasons).toContain("stale_or_missing_quote_context");
|
||||
});
|
||||
|
||||
it("uses timestamp-available event calendar matches for event-driven scoring", () => {
|
||||
const packet = buildFlowPacket({
|
||||
id: "flowpacket:event-driven",
|
||||
source_ts: Date.parse("2025-01-15T15:00:00Z"),
|
||||
features: {
|
||||
option_contract_id: "AAPL-2025-02-07-225-C",
|
||||
underlying_id: "AAPL",
|
||||
count: 1,
|
||||
window_ms: 450,
|
||||
total_size: 1800,
|
||||
total_premium: 160_000,
|
||||
total_notional: 16_000_000,
|
||||
nbbo_coverage_ratio: 0.5,
|
||||
nbbo_aggressive_ratio: 0.4,
|
||||
nbbo_aggressive_buy_ratio: 0.4,
|
||||
nbbo_aggressive_sell_ratio: 0.1,
|
||||
nbbo_inside_ratio: 0.08,
|
||||
underlying_mid: 224
|
||||
}
|
||||
});
|
||||
|
||||
const event = buildSmartMoneyEventFromPacket(packet, {
|
||||
eventCalendarMatch: {
|
||||
underlying_id: "AAPL",
|
||||
event_ts: Date.parse("2025-01-31T21:00:00Z"),
|
||||
event_kind: "earnings",
|
||||
announced_ts: Date.parse("2024-12-20T21:00:00Z"),
|
||||
days_to_event: 16.25
|
||||
}
|
||||
});
|
||||
|
||||
expect(event.features.days_to_event).toBeCloseTo(16.25);
|
||||
expect(event.features.expiry_after_event).toBe(true);
|
||||
expect(event.primary_profile_id).toBe("event_driven");
|
||||
});
|
||||
|
||||
it("keeps event-calendar features neutral when no match is available", () => {
|
||||
const packet = buildFlowPacket({
|
||||
id: "flowpacket:no-calendar",
|
||||
source_ts: Date.parse("2025-01-15T15:00:00Z"),
|
||||
features: {
|
||||
option_contract_id: "AAPL-2025-02-07-225-C",
|
||||
underlying_id: "AAPL",
|
||||
total_premium: 160_000,
|
||||
nbbo_coverage_ratio: 0.92
|
||||
}
|
||||
});
|
||||
|
||||
const event = buildSmartMoneyEventFromPacket(packet);
|
||||
expect(event.features.days_to_event).toBeNull();
|
||||
expect(event.features.expiry_after_event).toBeNull();
|
||||
expect(event.features.pre_event_concentration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue