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

@ -2,6 +2,9 @@
"name": "@islandflow/refdata",
"private": true,
"type": "module",
"exports": {
"./event-calendar": "./src/event-calendar.ts"
},
"scripts": {
"dev": "bun run src/index.ts"
},

View file

@ -0,0 +1,116 @@
export type EventCalendarKind = "earnings" | "dividend" | "corporate_action" | "m_and_a" | "news" | "other";
export type EventCalendarEntry = {
underlying_id: string;
event_ts: number;
event_kind: EventCalendarKind;
announced_ts: number;
source?: string;
source_event_id?: string;
};
export type EventCalendarMatch = EventCalendarEntry & {
days_to_event: number;
};
export type EventCalendarProvider = {
findNextEvent(underlyingId: string, asOfTs: number): EventCalendarMatch | null;
};
const MS_PER_DAY = 86_400_000;
const EVENT_KINDS = new Set<EventCalendarKind>([
"earnings",
"dividend",
"corporate_action",
"m_and_a",
"news",
"other"
]);
const normalizeUnderlying = (underlyingId: string): string => underlyingId.trim().toUpperCase();
const asNumber = (value: unknown): number | null => {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
const ts = Date.parse(value);
return Number.isFinite(ts) ? ts : null;
}
return null;
};
const asString = (value: unknown): string | null => (typeof value === "string" && value.trim() ? value.trim() : null);
export const parseEventCalendarEntries = (value: unknown): EventCalendarEntry[] => {
const rows = Array.isArray(value) ? value : [];
return rows.flatMap((row): EventCalendarEntry[] => {
if (!row || typeof row !== "object") {
return [];
}
const record = row as Record<string, unknown>;
const underlying = asString(record.underlying_id ?? record.underlying ?? record.symbol);
const eventTs = asNumber(record.event_ts ?? record.event_time ?? record.event_date);
const announcedTs = asNumber(record.announced_ts ?? record.available_ts ?? record.as_of_ts ?? record.created_ts) ?? 0;
const rawKind = asString(record.event_kind ?? record.kind ?? record.type) ?? "other";
const eventKind = EVENT_KINDS.has(rawKind as EventCalendarKind) ? (rawKind as EventCalendarKind) : "other";
if (!underlying || eventTs === null || eventTs < 0 || announcedTs < 0) {
return [];
}
return [
{
underlying_id: normalizeUnderlying(underlying),
event_ts: Math.trunc(eventTs),
event_kind: eventKind,
announced_ts: Math.trunc(announcedTs),
...(asString(record.source) ? { source: asString(record.source) ?? undefined } : {}),
...(asString(record.source_event_id ?? record.id)
? { source_event_id: asString(record.source_event_id ?? record.id) ?? undefined }
: {})
}
];
});
};
export const createStaticEventCalendarProvider = (entries: EventCalendarEntry[]): EventCalendarProvider => {
const byUnderlying = new Map<string, EventCalendarEntry[]>();
for (const entry of entries) {
const key = normalizeUnderlying(entry.underlying_id);
const normalized = { ...entry, underlying_id: key };
const bucket = byUnderlying.get(key) ?? [];
bucket.push(normalized);
byUnderlying.set(key, bucket);
}
for (const bucket of byUnderlying.values()) {
bucket.sort((a, b) => a.event_ts - b.event_ts || a.announced_ts - b.announced_ts);
}
return {
findNextEvent(underlyingId, asOfTs) {
const key = normalizeUnderlying(underlyingId);
if (!key || !Number.isFinite(asOfTs)) {
return null;
}
const bucket = byUnderlying.get(key) ?? [];
const entry = bucket.find((candidate) => candidate.announced_ts <= asOfTs && candidate.event_ts >= asOfTs);
return entry ? { ...entry, days_to_event: (entry.event_ts - asOfTs) / MS_PER_DAY } : null;
}
};
};
export const createEmptyEventCalendarProvider = (): EventCalendarProvider => createStaticEventCalendarProvider([]);
export const loadEventCalendarProviderFromFile = async (path: string): Promise<EventCalendarProvider> => {
const text = await Bun.file(path).text();
return createStaticEventCalendarProvider(parseEventCalendarEntries(JSON.parse(text)));
};

View file

@ -1,10 +1,28 @@
import { createLogger } from "@islandflow/observability";
import { createEmptyEventCalendarProvider, loadEventCalendarProviderFromFile } from "./event-calendar";
const service = "refdata";
const logger = createLogger({ service });
logger.info("service starting");
const eventCalendarPath = process.env.REFDATA_EVENT_CALENDAR_PATH ?? process.env.SMART_MONEY_EVENT_CALENDAR_PATH;
if (eventCalendarPath) {
try {
await loadEventCalendarProviderFromFile(eventCalendarPath);
logger.info("event calendar loaded", { path: eventCalendarPath });
} catch (error) {
logger.warn("event calendar unavailable", {
path: eventCalendarPath,
error: error instanceof Error ? error.message : String(error)
});
}
} else {
createEmptyEventCalendarProvider();
logger.info("event calendar disabled");
}
const shutdown = (signal: string) => {
logger.info("service stopping", { signal });
process.exit(0);

View file

@ -0,0 +1,31 @@
import { describe, expect, it } from "bun:test";
import { createStaticEventCalendarProvider, parseEventCalendarEntries } from "../src/event-calendar";
describe("event calendar refdata", () => {
it("parses provider rows and filters by timestamp availability", () => {
const entries = parseEventCalendarEntries([
{
symbol: "aapl",
event_date: "2025-01-31T21:00:00Z",
event_kind: "earnings",
announced_ts: "2025-01-20T21:00:00Z",
source: "fixture"
},
{
symbol: "AAPL",
event_date: "2025-02-28T21:00:00Z",
type: "mystery",
announced_ts: "2025-02-01T21:00:00Z"
}
]);
const provider = createStaticEventCalendarProvider(entries);
const beforeAnnouncement = provider.findNextEvent("AAPL", Date.parse("2025-01-15T15:00:00Z"));
const afterAnnouncement = provider.findNextEvent("aapl", Date.parse("2025-01-21T15:00:00Z"));
expect(beforeAnnouncement).toBeNull();
expect(afterAnnouncement?.event_kind).toBe("earnings");
expect(afterAnnouncement?.underlying_id).toBe("AAPL");
expect(afterAnnouncement?.days_to_event).toBeGreaterThan(0);
});
});