Unify live session streaming and evidence fetching
- Route live terminal data through a shared live session socket - Fetch missing evidence for alerts and classifier hits - Add live type definitions and storage/API tests
This commit is contained in:
parent
824b7f2fa0
commit
d30513119a
10 changed files with 1923 additions and 258 deletions
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./events";
|
||||
export * from "./live";
|
||||
export * from "./sp500";
|
||||
|
|
|
|||
182
packages/types/src/live.ts
Normal file
182
packages/types/src/live.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
AlertEventSchema,
|
||||
ClassifierHitEventSchema,
|
||||
EquityCandleSchema,
|
||||
EquityPrintJoinSchema,
|
||||
EquityPrintSchema,
|
||||
FlowPacketSchema,
|
||||
InferredDarkEventSchema,
|
||||
OptionNBBOSchema,
|
||||
OptionPrintSchema
|
||||
} from "./events";
|
||||
|
||||
export const CursorSchema = z.object({
|
||||
ts: z.number().int().nonnegative(),
|
||||
seq: z.number().int().nonnegative()
|
||||
});
|
||||
|
||||
export type Cursor = z.infer<typeof CursorSchema>;
|
||||
|
||||
export const LiveGenericChannelSchema = z.enum([
|
||||
"options",
|
||||
"nbbo",
|
||||
"equities",
|
||||
"equity-joins",
|
||||
"flow",
|
||||
"classifier-hits",
|
||||
"alerts",
|
||||
"inferred-dark"
|
||||
]);
|
||||
|
||||
export const LiveChannelSchema = z.enum([
|
||||
"options",
|
||||
"nbbo",
|
||||
"equities",
|
||||
"equity-joins",
|
||||
"flow",
|
||||
"classifier-hits",
|
||||
"alerts",
|
||||
"inferred-dark",
|
||||
"equity-candles",
|
||||
"equity-overlay"
|
||||
]);
|
||||
|
||||
export type LiveChannel = z.infer<typeof LiveChannelSchema>;
|
||||
export type LiveGenericChannel = z.infer<typeof LiveGenericChannelSchema>;
|
||||
|
||||
export const LiveSubscriptionSchema = z.discriminatedUnion("channel", [
|
||||
z.object({
|
||||
channel: LiveGenericChannelSchema
|
||||
}),
|
||||
z.object({
|
||||
channel: z.literal("equity-candles"),
|
||||
underlying_id: z.string().min(1),
|
||||
interval_ms: z.number().int().positive()
|
||||
}),
|
||||
z.object({
|
||||
channel: z.literal("equity-overlay"),
|
||||
underlying_id: z.string().min(1)
|
||||
})
|
||||
]);
|
||||
|
||||
export type LiveSubscription = z.infer<typeof LiveSubscriptionSchema>;
|
||||
|
||||
const livePayloadSchemas = {
|
||||
options: OptionPrintSchema,
|
||||
nbbo: OptionNBBOSchema,
|
||||
equities: EquityPrintSchema,
|
||||
"equity-joins": EquityPrintJoinSchema,
|
||||
flow: FlowPacketSchema,
|
||||
"classifier-hits": ClassifierHitEventSchema,
|
||||
alerts: AlertEventSchema,
|
||||
"inferred-dark": InferredDarkEventSchema,
|
||||
"equity-candles": EquityCandleSchema,
|
||||
"equity-overlay": EquityPrintSchema
|
||||
} as const;
|
||||
|
||||
export const FeedSnapshotSchema = z.object({
|
||||
subscription: LiveSubscriptionSchema,
|
||||
items: z.array(z.unknown()),
|
||||
watermark: CursorSchema.nullable(),
|
||||
next_before: CursorSchema.nullable()
|
||||
});
|
||||
|
||||
export type FeedSnapshot<T> = {
|
||||
subscription: LiveSubscription;
|
||||
items: T[];
|
||||
watermark: Cursor | null;
|
||||
next_before: Cursor | null;
|
||||
};
|
||||
|
||||
export const LiveSubscribeMessageSchema = z.object({
|
||||
op: z.literal("subscribe"),
|
||||
subscriptions: z.array(LiveSubscriptionSchema).min(1)
|
||||
});
|
||||
|
||||
export type LiveSubscribeMessage = z.infer<typeof LiveSubscribeMessageSchema>;
|
||||
|
||||
export const LiveUnsubscribeMessageSchema = z.object({
|
||||
op: z.literal("unsubscribe"),
|
||||
subscriptions: z.array(LiveSubscriptionSchema).min(1)
|
||||
});
|
||||
|
||||
export type LiveUnsubscribeMessage = z.infer<typeof LiveUnsubscribeMessageSchema>;
|
||||
|
||||
export const LivePingMessageSchema = z.object({
|
||||
op: z.literal("ping")
|
||||
});
|
||||
|
||||
export type LivePingMessage = z.infer<typeof LivePingMessageSchema>;
|
||||
|
||||
export const LiveClientMessageSchema = z.discriminatedUnion("op", [
|
||||
LiveSubscribeMessageSchema,
|
||||
LiveUnsubscribeMessageSchema,
|
||||
LivePingMessageSchema
|
||||
]);
|
||||
|
||||
export type LiveClientMessage = z.infer<typeof LiveClientMessageSchema>;
|
||||
|
||||
export const LiveReadyMessageSchema = z.object({
|
||||
op: z.literal("ready")
|
||||
});
|
||||
|
||||
export type LiveReadyMessage = z.infer<typeof LiveReadyMessageSchema>;
|
||||
|
||||
export const LiveSnapshotMessageSchema = z.object({
|
||||
op: z.literal("snapshot"),
|
||||
snapshot: FeedSnapshotSchema
|
||||
});
|
||||
|
||||
export type LiveSnapshotMessage = z.infer<typeof LiveSnapshotMessageSchema>;
|
||||
|
||||
export const LiveEventMessageSchema = z.object({
|
||||
op: z.literal("event"),
|
||||
subscription: LiveSubscriptionSchema,
|
||||
item: z.unknown(),
|
||||
watermark: CursorSchema.nullable()
|
||||
});
|
||||
|
||||
export type LiveEventMessage = z.infer<typeof LiveEventMessageSchema>;
|
||||
|
||||
export const LiveHeartbeatMessageSchema = z.object({
|
||||
op: z.literal("heartbeat"),
|
||||
ts: z.number().int().nonnegative()
|
||||
});
|
||||
|
||||
export type LiveHeartbeatMessage = z.infer<typeof LiveHeartbeatMessageSchema>;
|
||||
|
||||
export const LiveErrorMessageSchema = z.object({
|
||||
op: z.literal("error"),
|
||||
message: z.string().min(1)
|
||||
});
|
||||
|
||||
export type LiveErrorMessage = z.infer<typeof LiveErrorMessageSchema>;
|
||||
|
||||
export const LiveServerMessageSchema = z.discriminatedUnion("op", [
|
||||
LiveReadyMessageSchema,
|
||||
LiveSnapshotMessageSchema,
|
||||
LiveEventMessageSchema,
|
||||
LiveHeartbeatMessageSchema,
|
||||
LiveErrorMessageSchema
|
||||
]);
|
||||
|
||||
export type LiveServerMessage = z.infer<typeof LiveServerMessageSchema>;
|
||||
|
||||
export const getSubscriptionKey = (subscription: LiveSubscription): string => {
|
||||
switch (subscription.channel) {
|
||||
case "equity-candles":
|
||||
return `${subscription.channel}|${subscription.underlying_id}|${subscription.interval_ms}`;
|
||||
case "equity-overlay":
|
||||
return `${subscription.channel}|${subscription.underlying_id}`;
|
||||
default:
|
||||
return subscription.channel;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseLivePayload = (
|
||||
channel: LiveChannel,
|
||||
item: unknown
|
||||
): z.infer<(typeof livePayloadSchemas)[typeof channel]> => {
|
||||
return livePayloadSchemas[channel].parse(item);
|
||||
};
|
||||
69
packages/types/tests/live.test.ts
Normal file
69
packages/types/tests/live.test.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
CursorSchema,
|
||||
LiveClientMessageSchema,
|
||||
LiveServerMessageSchema,
|
||||
getSubscriptionKey
|
||||
} from "../src/live";
|
||||
|
||||
describe("live protocol types", () => {
|
||||
it("builds stable keys for generic and parameterized subscriptions", () => {
|
||||
expect(getSubscriptionKey({ channel: "flow" })).toBe("flow");
|
||||
expect(
|
||||
getSubscriptionKey({
|
||||
channel: "equity-candles",
|
||||
underlying_id: "SPY",
|
||||
interval_ms: 60000
|
||||
})
|
||||
).toBe("equity-candles|SPY|60000");
|
||||
expect(getSubscriptionKey({ channel: "equity-overlay", underlying_id: "SPY" })).toBe(
|
||||
"equity-overlay|SPY"
|
||||
);
|
||||
});
|
||||
|
||||
it("validates subscribe messages", () => {
|
||||
const parsed = LiveClientMessageSchema.parse({
|
||||
op: "subscribe",
|
||||
subscriptions: [
|
||||
{ channel: "flow" },
|
||||
{ channel: "equity-candles", underlying_id: "SPY", interval_ms: 60000 }
|
||||
]
|
||||
});
|
||||
|
||||
expect(parsed.op).toBe("subscribe");
|
||||
expect(parsed.subscriptions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("validates snapshot and event server messages", () => {
|
||||
const cursor = CursorSchema.parse({ ts: 100, seq: 2 });
|
||||
const snapshot = LiveServerMessageSchema.parse({
|
||||
op: "snapshot",
|
||||
snapshot: {
|
||||
subscription: { channel: "alerts" },
|
||||
items: [],
|
||||
watermark: cursor,
|
||||
next_before: null
|
||||
}
|
||||
});
|
||||
const event = LiveServerMessageSchema.parse({
|
||||
op: "event",
|
||||
subscription: { channel: "equity-overlay", underlying_id: "SPY" },
|
||||
item: {
|
||||
source_ts: 100,
|
||||
ingest_ts: 101,
|
||||
seq: 1,
|
||||
trace_id: "eq-1",
|
||||
ts: 100,
|
||||
underlying_id: "SPY",
|
||||
price: 500,
|
||||
size: 10,
|
||||
exchange: "X",
|
||||
offExchangeFlag: true
|
||||
},
|
||||
watermark: cursor
|
||||
});
|
||||
|
||||
expect(snapshot.op).toBe("snapshot");
|
||||
expect(event.op).toBe("event");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue