Fix contract-focused options tape hydration
This commit is contained in:
parent
73e25ddf70
commit
b73e62bdba
8 changed files with 657 additions and 171 deletions
|
|
@ -82,7 +82,7 @@ import {
|
|||
fetchClassifierHitsByPacketIds,
|
||||
fetchRecentOptionPrints
|
||||
} from "@islandflow/storage";
|
||||
import type { EquityPrintQueryFilters, OptionPrintQueryFilters } from "@islandflow/storage";
|
||||
import type { EquityPrintQueryFilters } from "@islandflow/storage";
|
||||
import {
|
||||
AlertEventSchema,
|
||||
ClassifierHitEventSchema,
|
||||
|
|
@ -99,11 +99,6 @@ import {
|
|||
LiveSubscriptionSchema,
|
||||
matchesFlowPacketFilters,
|
||||
matchesOptionPrintFilters,
|
||||
OptionFlowFilters,
|
||||
OptionFlowViewSchema,
|
||||
OptionNbboSideSchema,
|
||||
OptionSecurityTypeSchema,
|
||||
OptionTypeSchema,
|
||||
FlowPacketSchema,
|
||||
SmartMoneyEventSchema,
|
||||
OptionNBBOSchema,
|
||||
|
|
@ -113,6 +108,7 @@ import {
|
|||
import { createClient } from "redis";
|
||||
import { z } from "zod";
|
||||
import { HOT_LIVE_REDIS_KEYS, LiveStateManager, shouldFanoutLiveEvent } from "./live";
|
||||
import { parseOptionPrintQuery } from "./option-queries";
|
||||
|
||||
const service = "api";
|
||||
const logger = createLogger({ service });
|
||||
|
|
@ -224,33 +220,6 @@ const equityPrintRangeSchema = z.object({
|
|||
end_ts: z.coerce.number().int().nonnegative(),
|
||||
limit: limitSchema.optional()
|
||||
});
|
||||
const optionSideListSchema = z
|
||||
.string()
|
||||
.transform((value) =>
|
||||
value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
.pipe(z.array(OptionNbboSideSchema));
|
||||
const optionTypeListSchema = z
|
||||
.string()
|
||||
.transform((value) =>
|
||||
value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
.pipe(z.array(OptionTypeSchema));
|
||||
const optionSecuritySchema = z.enum(["stock", "etf", "all"]);
|
||||
const optionFilterQuerySchema = z.object({
|
||||
view: OptionFlowViewSchema.optional(),
|
||||
security: optionSecuritySchema.optional(),
|
||||
side: optionSideListSchema.optional(),
|
||||
type: optionTypeListSchema.optional(),
|
||||
min_notional: z.coerce.number().nonnegative().optional()
|
||||
});
|
||||
|
||||
type Channel =
|
||||
| "options"
|
||||
| "options-nbbo"
|
||||
|
|
@ -351,43 +320,6 @@ const applyDeliverPolicy = (
|
|||
}
|
||||
};
|
||||
|
||||
const parseOptionPrintFilters = (
|
||||
url: URL
|
||||
): {
|
||||
view: z.infer<typeof OptionFlowViewSchema>;
|
||||
storageFilters: Parameters<typeof fetchRecentOptionPrints>[3];
|
||||
liveFilters: OptionFlowFilters;
|
||||
} => {
|
||||
const parsed = optionFilterQuerySchema.parse({
|
||||
view: url.searchParams.get("view") ?? undefined,
|
||||
security: url.searchParams.get("security") ?? undefined,
|
||||
side: url.searchParams.get("side") ?? undefined,
|
||||
type: url.searchParams.get("type") ?? undefined,
|
||||
min_notional: url.searchParams.get("min_notional") ?? undefined
|
||||
});
|
||||
const view = parsed.view ?? "signal";
|
||||
const security = parsed.security ?? (view === "raw" ? "all" : "stock");
|
||||
const storageFilters = {
|
||||
view,
|
||||
security,
|
||||
minNotional: parsed.min_notional,
|
||||
nbboSides: parsed.side,
|
||||
optionTypes: parsed.type
|
||||
} as const;
|
||||
const liveFilters: OptionFlowFilters = {
|
||||
view,
|
||||
securityTypes:
|
||||
security === "all"
|
||||
? undefined
|
||||
: ([security] as Array<z.infer<typeof OptionSecurityTypeSchema>>),
|
||||
nbboSides: parsed.side,
|
||||
optionTypes: parsed.type,
|
||||
minNotional: parsed.min_notional
|
||||
};
|
||||
|
||||
return { view, storageFilters, liveFilters };
|
||||
};
|
||||
|
||||
const parseReplayParams = (url: URL): { afterTs: number; afterSeq: number; limit: number } => {
|
||||
const params = replayParamsSchema.parse({
|
||||
after_ts: url.searchParams.get("after_ts") ?? undefined,
|
||||
|
|
@ -605,15 +537,6 @@ const parseScopeList = (url: URL, ...keys: string[]): string[] | undefined => {
|
|||
return unique.length > 0 ? unique : undefined;
|
||||
};
|
||||
|
||||
const parseLiveOptionPrintFilters = (url: URL): OptionPrintQueryFilters => {
|
||||
const { storageFilters } = parseOptionPrintFilters(url);
|
||||
return {
|
||||
...storageFilters,
|
||||
underlyingIds: parseScopeList(url, "underlying_id", "underlying_ids"),
|
||||
optionContractId: url.searchParams.get("option_contract_id") ?? undefined
|
||||
};
|
||||
};
|
||||
|
||||
const parseLiveEquityPrintFilters = (url: URL): EquityPrintQueryFilters => ({
|
||||
underlyingIds: parseScopeList(url, "underlying_id", "underlying_ids")
|
||||
});
|
||||
|
|
@ -1399,7 +1322,7 @@ const run = async () => {
|
|||
try {
|
||||
const limit = parseLimit(url.searchParams.get("limit"));
|
||||
const source = parseReplaySource(url) ?? undefined;
|
||||
const { storageFilters } = parseOptionPrintFilters(url);
|
||||
const { storageFilters } = parseOptionPrintQuery(url);
|
||||
const data = await fetchRecentOptionPrints(clickhouse, limit, source, storageFilters);
|
||||
return jsonResponse({ data });
|
||||
} catch (error) {
|
||||
|
|
@ -1525,7 +1448,7 @@ const run = async () => {
|
|||
try {
|
||||
const { beforeTs, beforeSeq, limit } = parseBeforeParams(url);
|
||||
const source = parseReplaySource(url) ?? undefined;
|
||||
const storageFilters = parseLiveOptionPrintFilters(url);
|
||||
const { storageFilters } = parseOptionPrintQuery(url);
|
||||
const data = await fetchOptionPrintsBefore(
|
||||
clickhouse,
|
||||
beforeTs,
|
||||
|
|
@ -1668,7 +1591,7 @@ const run = async () => {
|
|||
try {
|
||||
const { afterTs, afterSeq, limit } = parseReplayParams(url);
|
||||
const source = parseReplaySource(url) ?? undefined;
|
||||
const { storageFilters } = parseOptionPrintFilters(url);
|
||||
const { storageFilters } = parseOptionPrintQuery(url);
|
||||
const data = await fetchOptionPrintsAfter(
|
||||
clickhouse,
|
||||
afterTs,
|
||||
|
|
|
|||
|
|
@ -345,6 +345,30 @@ const snapshotLimitFor = (subscription: LiveSubscription, configuredLimit: numbe
|
|||
return Math.max(1, Math.min(configuredLimit, Math.floor(requested)));
|
||||
};
|
||||
|
||||
export const buildOptionSnapshotFilters = (
|
||||
subscription: Extract<LiveSubscription, { channel: "options" }>
|
||||
): OptionPrintQueryFilters => {
|
||||
if (subscription.option_contract_id) {
|
||||
return {
|
||||
view: "raw",
|
||||
optionContractId: subscription.option_contract_id
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
view: subscription.filters?.view ?? "signal",
|
||||
security:
|
||||
subscription.filters?.securityTypes?.length === 1
|
||||
? subscription.filters.securityTypes[0]
|
||||
: "all",
|
||||
nbboSides: subscription.filters?.nbboSides,
|
||||
optionTypes: subscription.filters?.optionTypes,
|
||||
minNotional: subscription.filters?.minNotional,
|
||||
underlyingIds: subscription.underlying_ids,
|
||||
optionContractId: subscription.option_contract_id
|
||||
};
|
||||
};
|
||||
|
||||
const candleRedisKey = (underlyingId: string, intervalMs: number): string =>
|
||||
`live:equity-candles:${underlyingId}:${intervalMs}`;
|
||||
|
||||
|
|
@ -489,18 +513,7 @@ export class LiveStateManager {
|
|||
if (subscription.filters?.view === "raw" || scoped) {
|
||||
this.stats.scopedClickHouseSnapshots += 1;
|
||||
const limit = snapshotLimitFor(subscription, this.generic.options.limit);
|
||||
const storageFilters: OptionPrintQueryFilters = {
|
||||
view: subscription.filters?.view ?? "signal",
|
||||
security:
|
||||
subscription.filters?.securityTypes?.length === 1
|
||||
? subscription.filters.securityTypes[0]
|
||||
: "all",
|
||||
nbboSides: subscription.filters?.nbboSides,
|
||||
optionTypes: subscription.filters?.optionTypes,
|
||||
minNotional: subscription.filters?.minNotional,
|
||||
underlyingIds: subscription.underlying_ids,
|
||||
optionContractId: subscription.option_contract_id
|
||||
};
|
||||
const storageFilters = buildOptionSnapshotFilters(subscription);
|
||||
const items = await fetchRecentOptionPrints(
|
||||
this.clickhouse,
|
||||
limit,
|
||||
|
|
|
|||
107
services/api/src/option-queries.ts
Normal file
107
services/api/src/option-queries.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import type { OptionPrintQueryFilters } from "@islandflow/storage";
|
||||
import {
|
||||
OptionFlowViewSchema,
|
||||
OptionNbboSideSchema,
|
||||
OptionSecurityTypeSchema,
|
||||
OptionTypeSchema,
|
||||
type OptionFlowFilters
|
||||
} from "@islandflow/types";
|
||||
import { z } from "zod";
|
||||
|
||||
const optionSideListSchema = z
|
||||
.string()
|
||||
.transform((value) =>
|
||||
value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
.pipe(z.array(OptionNbboSideSchema));
|
||||
|
||||
const optionTypeListSchema = z
|
||||
.string()
|
||||
.transform((value) =>
|
||||
value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
.pipe(z.array(OptionTypeSchema));
|
||||
|
||||
const optionSecuritySchema = z.enum(["stock", "etf", "all"]);
|
||||
|
||||
const optionFilterQuerySchema = z.object({
|
||||
view: OptionFlowViewSchema.optional(),
|
||||
security: optionSecuritySchema.optional(),
|
||||
side: optionSideListSchema.optional(),
|
||||
type: optionTypeListSchema.optional(),
|
||||
min_notional: z.coerce.number().nonnegative().optional()
|
||||
});
|
||||
|
||||
export type ParsedOptionPrintQuery = {
|
||||
scope: {
|
||||
underlyingIds?: string[];
|
||||
optionContractId?: string;
|
||||
};
|
||||
flowFilters: OptionFlowFilters;
|
||||
storageFilters: OptionPrintQueryFilters;
|
||||
isContractDrilldown: boolean;
|
||||
};
|
||||
|
||||
const parseScopeList = (url: URL, ...keys: string[]): string[] | undefined => {
|
||||
const values = keys
|
||||
.flatMap((key) => url.searchParams.getAll(key))
|
||||
.flatMap((value) => value.split(","))
|
||||
.map((value) => value.trim().toUpperCase())
|
||||
.filter(Boolean);
|
||||
const unique = Array.from(new Set(values));
|
||||
return unique.length > 0 ? unique : undefined;
|
||||
};
|
||||
|
||||
export const parseOptionPrintQuery = (url: URL): ParsedOptionPrintQuery => {
|
||||
const parsed = optionFilterQuerySchema.parse({
|
||||
view: url.searchParams.get("view") ?? undefined,
|
||||
security: url.searchParams.get("security") ?? undefined,
|
||||
side: url.searchParams.get("side") ?? undefined,
|
||||
type: url.searchParams.get("type") ?? undefined,
|
||||
min_notional: url.searchParams.get("min_notional") ?? undefined
|
||||
});
|
||||
const scope = {
|
||||
underlyingIds: parseScopeList(url, "underlying_id", "underlying_ids"),
|
||||
optionContractId: url.searchParams.get("option_contract_id") ?? undefined
|
||||
};
|
||||
const view = parsed.view ?? "signal";
|
||||
const security = parsed.security ?? (view === "raw" ? "all" : "stock");
|
||||
const flowFilters: OptionFlowFilters = {
|
||||
view,
|
||||
securityTypes:
|
||||
security === "all"
|
||||
? undefined
|
||||
: ([security] as Array<z.infer<typeof OptionSecurityTypeSchema>>),
|
||||
nbboSides: parsed.side,
|
||||
optionTypes: parsed.type,
|
||||
minNotional: parsed.min_notional
|
||||
};
|
||||
const isContractDrilldown = Boolean(scope.optionContractId);
|
||||
const storageFilters: OptionPrintQueryFilters = isContractDrilldown
|
||||
? {
|
||||
view: "raw",
|
||||
optionContractId: scope.optionContractId
|
||||
}
|
||||
: {
|
||||
view,
|
||||
security,
|
||||
minNotional: parsed.min_notional,
|
||||
nbboSides: parsed.side,
|
||||
optionTypes: parsed.type,
|
||||
underlyingIds: scope.underlyingIds,
|
||||
optionContractId: scope.optionContractId
|
||||
};
|
||||
|
||||
return {
|
||||
scope,
|
||||
flowFilters,
|
||||
storageFilters,
|
||||
isContractDrilldown
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue