This commit is contained in:
parent
65139bf8d0
commit
44431c4e66
71 changed files with 2262 additions and 1173 deletions
|
|
@ -465,8 +465,7 @@ const parseCandleParams = (
|
|||
|
||||
const endTs = params.end_ts ?? Date.now();
|
||||
const limit = params.limit ?? env.REST_DEFAULT_LIMIT;
|
||||
const startTs =
|
||||
params.start_ts ?? Math.max(0, Math.floor(endTs - params.interval_ms * limit));
|
||||
const startTs = params.start_ts ?? Math.max(0, Math.floor(endTs - params.interval_ms * limit));
|
||||
const rangeStart = Math.min(startTs, endTs);
|
||||
const rangeEnd = Math.max(startTs, endTs);
|
||||
|
||||
|
|
@ -482,7 +481,13 @@ const parseCandleParams = (
|
|||
|
||||
const parseCandleReplayParams = (
|
||||
url: URL
|
||||
): { underlyingId: string; intervalMs: number; afterTs: number; afterSeq: number; limit: number } => {
|
||||
): {
|
||||
underlyingId: string;
|
||||
intervalMs: number;
|
||||
afterTs: number;
|
||||
afterSeq: number;
|
||||
limit: number;
|
||||
} => {
|
||||
const params = candleReplaySchema.parse({
|
||||
underlying_id: url.searchParams.get("underlying_id") ?? undefined,
|
||||
interval_ms: url.searchParams.get("interval_ms") ?? undefined,
|
||||
|
|
@ -601,7 +606,10 @@ const matchesScopedOptionSubscription = (
|
|||
print: { underlying_id?: string; option_contract_id: string },
|
||||
subscription: Extract<LiveSubscription, { channel: "options" }>
|
||||
): boolean => {
|
||||
if (subscription.option_contract_id && subscription.option_contract_id !== print.option_contract_id) {
|
||||
if (
|
||||
subscription.option_contract_id &&
|
||||
subscription.option_contract_id !== print.option_contract_id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (subscription.underlying_ids?.length) {
|
||||
|
|
@ -693,8 +701,7 @@ const run = async () => {
|
|||
env.OPTIONS_INGEST_ADAPTER,
|
||||
env.EQUITIES_INGEST_ADAPTER
|
||||
);
|
||||
const syntheticBackendDisabledReason =
|
||||
getSyntheticBackendDisabledReason(syntheticBackendMode);
|
||||
const syntheticBackendDisabledReason = getSyntheticBackendDisabledReason(syntheticBackendMode);
|
||||
const syntheticControlKv = await openSyntheticControlKv(js);
|
||||
let syntheticControl = await ensureSyntheticControlState(syntheticControlKv);
|
||||
const syntheticProfileHits = createRollingSyntheticProfileHits();
|
||||
|
|
@ -899,11 +906,7 @@ const run = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const subscribeWithReset = async <T>(
|
||||
subject: string,
|
||||
stream: string,
|
||||
durableName: string
|
||||
) => {
|
||||
const subscribeWithReset = async <T>(subject: string, stream: string, durableName: string) => {
|
||||
const opts = buildDurableConsumer(durableName);
|
||||
applyDeliverPolicy(opts, env.API_DELIVER_POLICY);
|
||||
try {
|
||||
|
|
@ -924,7 +927,8 @@ const run = async () => {
|
|||
try {
|
||||
await jsm.consumers.delete(stream, durableName);
|
||||
} catch (deleteError) {
|
||||
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
const deleteMessage =
|
||||
deleteError instanceof Error ? deleteError.message : String(deleteError);
|
||||
if (!deleteMessage.includes("not found")) {
|
||||
logger.warn("failed to delete jetstream consumer", {
|
||||
durable: durableName,
|
||||
|
|
@ -1023,8 +1027,12 @@ const run = async () => {
|
|||
}
|
||||
|
||||
const matchingSubscriptions =
|
||||
subscription.channel === "options" || subscription.channel === "flow" || subscription.channel === "equities"
|
||||
? [...subscriptionDefinitions.entries()].filter(([, candidate]) => candidate.channel === subscription.channel)
|
||||
subscription.channel === "options" ||
|
||||
subscription.channel === "flow" ||
|
||||
subscription.channel === "equities"
|
||||
? [...subscriptionDefinitions.entries()].filter(
|
||||
([, candidate]) => candidate.channel === subscription.channel
|
||||
)
|
||||
: [[getSubscriptionKey(subscription), subscription] as const];
|
||||
|
||||
if (matchingSubscriptions.length === 0) {
|
||||
|
|
@ -1032,8 +1040,12 @@ const run = async () => {
|
|||
}
|
||||
|
||||
const optionItem = ingestChannel === "options" ? (item as OptionPrint) : null;
|
||||
const equityItem = ingestChannel === "equities" ? (item as Parameters<typeof matchesScopedEquitySubscription>[0]) : null;
|
||||
const flowItem = ingestChannel === "flow" ? (item as Parameters<typeof matchesFlowPacketFilters>[0]) : null;
|
||||
const equityItem =
|
||||
ingestChannel === "equities"
|
||||
? (item as Parameters<typeof matchesScopedEquitySubscription>[0])
|
||||
: null;
|
||||
const flowItem =
|
||||
ingestChannel === "flow" ? (item as Parameters<typeof matchesFlowPacketFilters>[0]) : null;
|
||||
let matchedSubscriptions = 0;
|
||||
|
||||
for (const [key, candidate] of matchingSubscriptions) {
|
||||
|
|
@ -1315,9 +1327,7 @@ const run = async () => {
|
|||
},
|
||||
control: syntheticBackendMode === "synthetic" ? syntheticControl : null,
|
||||
derived,
|
||||
...(syntheticBackendDisabledReason
|
||||
? { disabled_reason: syntheticBackendDisabledReason }
|
||||
: {})
|
||||
...(syntheticBackendDisabledReason ? { disabled_reason: syntheticBackendDisabledReason } : {})
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -1385,11 +1395,7 @@ const run = async () => {
|
|||
syntheticControl = await writeSyntheticControlState(syntheticControlKv, payload);
|
||||
return jsonResponse({
|
||||
control: syntheticControl,
|
||||
derived: buildSyntheticDerivedStatus(
|
||||
Date.now(),
|
||||
syntheticControl,
|
||||
syntheticProfileHits
|
||||
)
|
||||
derived: buildSyntheticDerivedStatus(Date.now(), syntheticControl, syntheticProfileHits)
|
||||
});
|
||||
} catch (error) {
|
||||
return jsonResponse(
|
||||
|
|
@ -1436,7 +1442,13 @@ const run = async () => {
|
|||
if (req.method === "GET" && url.pathname === "/prints/equities/range") {
|
||||
try {
|
||||
const { underlyingId, startTs, endTs, limit } = parseEquityPrintRangeParams(url);
|
||||
const data = await fetchEquityPrintsRange(clickhouse, underlyingId, startTs, endTs, limit);
|
||||
const data = await fetchEquityPrintsRange(
|
||||
clickhouse,
|
||||
underlyingId,
|
||||
startTs,
|
||||
endTs,
|
||||
limit
|
||||
);
|
||||
return jsonResponse({ data });
|
||||
} catch (error) {
|
||||
return jsonResponse(
|
||||
|
|
@ -1566,7 +1578,9 @@ const run = async () => {
|
|||
source,
|
||||
storageFilters
|
||||
);
|
||||
return jsonResponse(buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq })));
|
||||
return jsonResponse(
|
||||
buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq }))
|
||||
);
|
||||
} catch (error) {
|
||||
return jsonResponse(
|
||||
{
|
||||
|
|
@ -1986,7 +2000,9 @@ const run = async () => {
|
|||
const payload =
|
||||
typeof message === "string"
|
||||
? message
|
||||
: new TextDecoder().decode(message instanceof Uint8Array ? message : new Uint8Array(message));
|
||||
: new TextDecoder().decode(
|
||||
message instanceof Uint8Array ? message : new Uint8Array(message)
|
||||
);
|
||||
const parsed = LiveClientMessageSchema.parse(JSON.parse(payload));
|
||||
if (parsed.op === "ping") {
|
||||
sendLiveMessage(socket, {
|
||||
|
|
|
|||
|
|
@ -165,11 +165,21 @@ const parseGenericLimitFallback = (env: NodeJS.ProcessEnv, fallback: number): nu
|
|||
return Math.max(MIN_GENERIC_LIMIT, Math.min(MAX_GENERIC_LIMIT, Math.floor(parsed)));
|
||||
};
|
||||
|
||||
export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env): GenericLiveLimits => {
|
||||
export const resolveGenericLiveLimits = (
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): GenericLiveLimits => {
|
||||
const liveLimitDefault = parseGenericLimitFallback(env, DEFAULT_GENERIC_LIMIT);
|
||||
return {
|
||||
options: parseGenericLimit(env, "options", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.options),
|
||||
nbbo: parseGenericLimit(env, "nbbo", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.nbbo),
|
||||
options: parseGenericLimit(
|
||||
env,
|
||||
"options",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.options
|
||||
),
|
||||
nbbo: parseGenericLimit(
|
||||
env,
|
||||
"nbbo",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.nbbo
|
||||
),
|
||||
equities: parseGenericLimit(
|
||||
env,
|
||||
"equities",
|
||||
|
|
@ -185,7 +195,11 @@ export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env):
|
|||
"equity-joins",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["equity-joins"]
|
||||
),
|
||||
flow: parseGenericLimit(env, "flow", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.flow),
|
||||
flow: parseGenericLimit(
|
||||
env,
|
||||
"flow",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.flow
|
||||
),
|
||||
"smart-money": parseGenericLimit(
|
||||
env,
|
||||
"smart-money",
|
||||
|
|
@ -196,13 +210,21 @@ export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env):
|
|||
"classifier-hits",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["classifier-hits"]
|
||||
),
|
||||
alerts: parseGenericLimit(env, "alerts", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.alerts),
|
||||
alerts: parseGenericLimit(
|
||||
env,
|
||||
"alerts",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.alerts
|
||||
),
|
||||
"inferred-dark": parseGenericLimit(
|
||||
env,
|
||||
"inferred-dark",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["inferred-dark"]
|
||||
),
|
||||
news: parseGenericLimit(env, "news", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.news)
|
||||
news: parseGenericLimit(
|
||||
env,
|
||||
"news",
|
||||
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.news
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -227,12 +249,18 @@ const extractFreshnessTs = (channel: LiveGenericChannel, item: any): number | nu
|
|||
|
||||
export const resolveLiveStateConfig = (env: NodeJS.ProcessEnv = process.env): LiveStateConfig => ({
|
||||
limits: resolveGenericLiveLimits(env),
|
||||
scopedCacheMaxKeys: parsePositiveInt(env.LIVE_SCOPED_CACHE_MAX_KEYS, DEFAULT_SCOPED_CACHE_MAX_KEYS),
|
||||
scopedCacheMaxKeys: parsePositiveInt(
|
||||
env.LIVE_SCOPED_CACHE_MAX_KEYS,
|
||||
DEFAULT_SCOPED_CACHE_MAX_KEYS
|
||||
),
|
||||
redisFlushIntervalMs: parsePositiveInt(
|
||||
env.LIVE_REDIS_FLUSH_INTERVAL_MS,
|
||||
DEFAULT_REDIS_FLUSH_INTERVAL_MS
|
||||
),
|
||||
redisFlushMaxItems: parsePositiveInt(env.LIVE_REDIS_FLUSH_MAX_ITEMS, DEFAULT_REDIS_FLUSH_MAX_ITEMS)
|
||||
redisFlushMaxItems: parsePositiveInt(
|
||||
env.LIVE_REDIS_FLUSH_MAX_ITEMS,
|
||||
DEFAULT_REDIS_FLUSH_MAX_ITEMS
|
||||
)
|
||||
});
|
||||
const parsePositiveInt = (value: string | undefined, fallback: number): number => {
|
||||
const parsed = Number(value);
|
||||
|
|
@ -242,10 +270,7 @@ const parsePositiveInt = (value: string | undefined, fallback: number): number =
|
|||
return Math.max(1, Math.floor(parsed));
|
||||
};
|
||||
|
||||
type RedisLike = Pick<
|
||||
RedisClientType,
|
||||
"isOpen" | "lRange" | "lPush" | "lTrim" | "hGet" | "hSet"
|
||||
>;
|
||||
type RedisLike = Pick<RedisClientType, "isOpen" | "lRange" | "lPush" | "lTrim" | "hGet" | "hSet">;
|
||||
|
||||
const parseCursor = (value: string | null): Cursor | null => {
|
||||
if (!value) {
|
||||
|
|
@ -259,7 +284,9 @@ const parseCursor = (value: string | null): Cursor | null => {
|
|||
}
|
||||
};
|
||||
|
||||
const getGenericConfig = (limits: GenericLiveLimits): {
|
||||
const getGenericConfig = (
|
||||
limits: GenericLiveLimits
|
||||
): {
|
||||
[K in LiveGenericChannel]: GenericFeedConfig;
|
||||
} => ({
|
||||
options: {
|
||||
|
|
@ -365,7 +392,7 @@ const parseJsonList = <T>(payloads: string[], parse: (value: unknown) => T): T[]
|
|||
return items;
|
||||
};
|
||||
|
||||
const compareCursors = (a: Cursor, b: Cursor): number => (b.ts - a.ts) || (b.seq - a.seq);
|
||||
const compareCursors = (a: Cursor, b: Cursor): number => b.ts - a.ts || b.seq - a.seq;
|
||||
|
||||
const sortGenericItems = <T>(items: T[], cursorOf: (item: T) => Cursor): T[] =>
|
||||
[...items].sort((a, b) => compareCursors(cursorOf(a), cursorOf(b)));
|
||||
|
|
@ -480,7 +507,10 @@ const matchesScopedOptionSnapshot = (
|
|||
return false;
|
||||
}
|
||||
|
||||
if (subscription.option_contract_id && item.option_contract_id !== subscription.option_contract_id) {
|
||||
if (
|
||||
subscription.option_contract_id &&
|
||||
item.option_contract_id !== subscription.option_contract_id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -529,11 +559,8 @@ const candleCursorField = (underlyingId: string, intervalMs: number): string =>
|
|||
const overlayRedisKey = (underlyingId: string): string => `live:equity-overlay:${underlyingId}`;
|
||||
const overlayCursorField = (underlyingId: string): string => `equities:${underlyingId}`;
|
||||
|
||||
const dropMatchingCursor = <T>(
|
||||
items: T[],
|
||||
target: Cursor,
|
||||
cursorOf: (item: T) => Cursor
|
||||
): T[] => items.filter((item) => compareCursors(cursorOf(item), target) !== 0);
|
||||
const dropMatchingCursor = <T>(items: T[], target: Cursor, cursorOf: (item: T) => Cursor): T[] =>
|
||||
items.filter((item) => compareCursors(cursorOf(item), target) !== 0);
|
||||
|
||||
const insertNewestFirst = <T>(
|
||||
items: T[],
|
||||
|
|
@ -676,7 +703,13 @@ export class LiveStateManager {
|
|||
this.pendingRedisWrites.clear();
|
||||
|
||||
for (const write of writes) {
|
||||
await this.persistList(write.listKey, write.cursorField, write.items, write.limit, write.cursor);
|
||||
await this.persistList(
|
||||
write.listKey,
|
||||
write.cursorField,
|
||||
write.items,
|
||||
write.limit,
|
||||
write.cursor
|
||||
);
|
||||
this.stats.redisFlushCount += 1;
|
||||
this.stats.redisFlushItems += write.items.length;
|
||||
metrics.count("api.live.redis_flush_count", 1);
|
||||
|
|
@ -726,7 +759,12 @@ export class LiveStateManager {
|
|||
}
|
||||
}
|
||||
|
||||
private updateFreshnessMetric(listKey: string, channel: LiveChannel, item: unknown, now = Date.now()): void {
|
||||
private updateFreshnessMetric(
|
||||
listKey: string,
|
||||
channel: LiveChannel,
|
||||
item: unknown,
|
||||
now = Date.now()
|
||||
): void {
|
||||
const ts =
|
||||
channel === "equity-candles" || channel === "equity-overlay"
|
||||
? typeof (item as { ts?: unknown })?.ts === "number"
|
||||
|
|
@ -784,12 +822,22 @@ export class LiveStateManager {
|
|||
config.cursorField,
|
||||
parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, config.cursorField))
|
||||
);
|
||||
await this.persistList(config.redisKey, config.cursorField, cached, config.limit, this.genericCursors.get(config.cursorField) ?? null);
|
||||
await this.persistList(
|
||||
config.redisKey,
|
||||
config.cursorField,
|
||||
cached,
|
||||
config.limit,
|
||||
this.genericCursors.get(config.cursorField) ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fresh = normalizeGenericItems(channel, await config.fetchRecent(this.clickhouse, config.limit), config);
|
||||
const fresh = normalizeGenericItems(
|
||||
channel,
|
||||
await config.fetchRecent(this.clickhouse, config.limit),
|
||||
config
|
||||
);
|
||||
this.stats.genericHydrateFromClickHouse += 1;
|
||||
this.stats.cacheDepthByKey.set(config.redisKey, fresh.length);
|
||||
this.genericItems.set(channel, fresh);
|
||||
|
|
@ -806,7 +854,8 @@ export class LiveStateManager {
|
|||
case "options": {
|
||||
const config = this.generic.options;
|
||||
const limit = snapshotLimitFor(subscription, config.limit);
|
||||
const scoped = Boolean(subscription.underlying_ids?.length) || Boolean(subscription.option_contract_id);
|
||||
const scoped =
|
||||
Boolean(subscription.underlying_ids?.length) || Boolean(subscription.option_contract_id);
|
||||
if (subscription.filters?.view === "raw" || scoped) {
|
||||
const cached = (this.genericItems.get("options") ?? [])
|
||||
.filter((entry) => matchesScopedOptionSnapshot(entry, subscription))
|
||||
|
|
@ -815,8 +864,16 @@ export class LiveStateManager {
|
|||
if (cached.length < limit) {
|
||||
this.stats.scopedClickHouseSnapshots += 1;
|
||||
const storageFilters = buildOptionSnapshotFilters(subscription);
|
||||
const backfill = await fetchRecentOptionPrints(this.clickhouse, limit, undefined, storageFilters);
|
||||
items = mergeSnapshotBackfill(cached, backfill, limit, (entry) => ({ ts: entry.ts, seq: entry.seq }));
|
||||
const backfill = await fetchRecentOptionPrints(
|
||||
this.clickhouse,
|
||||
limit,
|
||||
undefined,
|
||||
storageFilters
|
||||
);
|
||||
items = mergeSnapshotBackfill(cached, backfill, limit, (entry) => ({
|
||||
ts: entry.ts,
|
||||
seq: entry.seq
|
||||
}));
|
||||
}
|
||||
return {
|
||||
subscription,
|
||||
|
|
@ -942,7 +999,11 @@ export class LiveStateManager {
|
|||
this.candleItems.set(key, nextState.items);
|
||||
this.candleCursors.set(cursorField, cursor);
|
||||
this.touchAccess(this.candleAccess, key);
|
||||
this.evictScopedCachesIfNeeded(this.candleItems as Map<string, unknown[]>, this.candleCursors, this.candleAccess);
|
||||
this.evictScopedCachesIfNeeded(
|
||||
this.candleItems as Map<string, unknown[]>,
|
||||
this.candleCursors,
|
||||
this.candleAccess
|
||||
);
|
||||
if (nextState.outOfOrder) {
|
||||
this.stats.outOfOrderEvents += 1;
|
||||
metrics.count("api.live.out_of_order_events", 1);
|
||||
|
|
@ -968,7 +1029,11 @@ export class LiveStateManager {
|
|||
this.overlayItems.set(key, nextState.items);
|
||||
this.overlayCursors.set(cursorField, cursor);
|
||||
this.touchAccess(this.overlayAccess, key);
|
||||
this.evictScopedCachesIfNeeded(this.overlayItems as Map<string, unknown[]>, this.overlayCursors, this.overlayAccess);
|
||||
this.evictScopedCachesIfNeeded(
|
||||
this.overlayItems as Map<string, unknown[]>,
|
||||
this.overlayCursors,
|
||||
this.overlayAccess
|
||||
);
|
||||
if (nextState.outOfOrder) {
|
||||
this.stats.outOfOrderEvents += 1;
|
||||
metrics.count("api.live.out_of_order_events", 1);
|
||||
|
|
@ -991,10 +1056,19 @@ export class LiveStateManager {
|
|||
const nextState =
|
||||
channel === "nbbo"
|
||||
? {
|
||||
items: normalizeGenericItems(channel, [parsed, ...(this.genericItems.get(channel) ?? [])], config),
|
||||
items: normalizeGenericItems(
|
||||
channel,
|
||||
[parsed, ...(this.genericItems.get(channel) ?? [])],
|
||||
config
|
||||
),
|
||||
outOfOrder: false
|
||||
}
|
||||
: insertNewestFirst(this.genericItems.get(channel) ?? [], parsed, config.cursor, config.limit);
|
||||
: insertNewestFirst(
|
||||
this.genericItems.get(channel) ?? [],
|
||||
parsed,
|
||||
config.cursor,
|
||||
config.limit
|
||||
);
|
||||
|
||||
if (nextState.outOfOrder) {
|
||||
this.stats.outOfOrderEvents += 1;
|
||||
|
|
@ -1007,7 +1081,13 @@ export class LiveStateManager {
|
|||
if (nextState.items.length > 0) {
|
||||
this.updateFreshnessMetric(config.redisKey, channel, nextState.items[0]);
|
||||
}
|
||||
this.queueRedisWrite(config.redisKey, config.cursorField, nextState.items, config.limit, cursor);
|
||||
this.queueRedisWrite(
|
||||
config.redisKey,
|
||||
config.cursorField,
|
||||
nextState.items,
|
||||
config.limit,
|
||||
cursor
|
||||
);
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
|
|
@ -1022,18 +1102,34 @@ export class LiveStateManager {
|
|||
if (cached.length > 0) {
|
||||
this.candleItems.set(key, cached);
|
||||
this.touchAccess(this.candleAccess, key);
|
||||
this.evictScopedCachesIfNeeded(this.candleItems as Map<string, unknown[]>, this.candleCursors, this.candleAccess);
|
||||
this.evictScopedCachesIfNeeded(
|
||||
this.candleItems as Map<string, unknown[]>,
|
||||
this.candleCursors,
|
||||
this.candleAccess
|
||||
);
|
||||
this.stats.cacheDepthByKey.set(key, cached.length);
|
||||
this.updateFreshnessMetric(key, "equity-candles", cached[0]);
|
||||
this.candleCursors.set(cursorField, parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField)));
|
||||
this.candleCursors.set(
|
||||
cursorField,
|
||||
parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField))
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fresh = await fetchRecentEquityCandles(this.clickhouse, underlyingId, intervalMs, CHART_LIMITS.candles);
|
||||
const fresh = await fetchRecentEquityCandles(
|
||||
this.clickhouse,
|
||||
underlyingId,
|
||||
intervalMs,
|
||||
CHART_LIMITS.candles
|
||||
);
|
||||
this.candleItems.set(key, fresh);
|
||||
this.touchAccess(this.candleAccess, key);
|
||||
this.evictScopedCachesIfNeeded(this.candleItems as Map<string, unknown[]>, this.candleCursors, this.candleAccess);
|
||||
this.evictScopedCachesIfNeeded(
|
||||
this.candleItems as Map<string, unknown[]>,
|
||||
this.candleCursors,
|
||||
this.candleAccess
|
||||
);
|
||||
this.stats.cacheDepthByKey.set(key, fresh.length);
|
||||
if (fresh.length > 0) {
|
||||
this.updateFreshnessMetric(key, "equity-candles", fresh[0]);
|
||||
|
|
@ -1052,10 +1148,17 @@ export class LiveStateManager {
|
|||
if (cached.length > 0) {
|
||||
this.overlayItems.set(key, cached);
|
||||
this.touchAccess(this.overlayAccess, key);
|
||||
this.evictScopedCachesIfNeeded(this.overlayItems as Map<string, unknown[]>, this.overlayCursors, this.overlayAccess);
|
||||
this.evictScopedCachesIfNeeded(
|
||||
this.overlayItems as Map<string, unknown[]>,
|
||||
this.overlayCursors,
|
||||
this.overlayAccess
|
||||
);
|
||||
this.stats.cacheDepthByKey.set(key, cached.length);
|
||||
this.updateFreshnessMetric(key, "equity-overlay", cached[0]);
|
||||
this.overlayCursors.set(cursorField, parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField)));
|
||||
this.overlayCursors.set(
|
||||
cursorField,
|
||||
parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField))
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1065,7 +1168,11 @@ export class LiveStateManager {
|
|||
);
|
||||
this.overlayItems.set(key, fresh);
|
||||
this.touchAccess(this.overlayAccess, key);
|
||||
this.evictScopedCachesIfNeeded(this.overlayItems as Map<string, unknown[]>, this.overlayCursors, this.overlayAccess);
|
||||
this.evictScopedCachesIfNeeded(
|
||||
this.overlayItems as Map<string, unknown[]>,
|
||||
this.overlayCursors,
|
||||
this.overlayAccess
|
||||
);
|
||||
this.stats.cacheDepthByKey.set(key, fresh.length);
|
||||
if (fresh.length > 0) {
|
||||
this.updateFreshnessMetric(key, "equity-overlay", fresh[0]);
|
||||
|
|
|
|||
|
|
@ -83,11 +83,7 @@ export const buildSyntheticDerivedStatus = (
|
|||
session_phase: session.session_phase,
|
||||
regime: session.regime,
|
||||
focus_symbols: session.focus_symbols,
|
||||
profile_hit_counts: getSyntheticProfileHitCounts(
|
||||
state,
|
||||
now,
|
||||
control.coverage_window_minutes
|
||||
),
|
||||
profile_hit_counts: getSyntheticProfileHitCounts(state, now, control.coverage_window_minutes),
|
||||
coverage_window_minutes: control.coverage_window_minutes
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { isAlertContextPath, parseAlertContextTraceIdPath } from "../src/alert-c
|
|||
|
||||
describe("alert context route helpers", () => {
|
||||
it("extracts a valid alert trace id from the context endpoint path", () => {
|
||||
expect(parseAlertContextTraceIdPath("/flow/alerts/alert%3Actx%2Fone/context")).toBe("alert:ctx/one");
|
||||
expect(parseAlertContextTraceIdPath("/flow/alerts/alert%3Actx%2Fone/context")).toBe(
|
||||
"alert:ctx/one"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null for unrelated alert paths", () => {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ import {
|
|||
shouldFanoutLiveEvent
|
||||
} from "../src/live";
|
||||
|
||||
const makeClickHouse = (
|
||||
queryResolver?: (query: string) => unknown[]
|
||||
): ClickHouseClient =>
|
||||
const makeClickHouse = (queryResolver?: (query: string) => unknown[]): ClickHouseClient =>
|
||||
({
|
||||
exec: async () => {},
|
||||
insert: async () => {},
|
||||
|
|
@ -149,22 +147,18 @@ describe("LiveStateManager", () => {
|
|||
it("trims generic windows to configured per-channel limits", async () => {
|
||||
const redis = makeRedis();
|
||||
const now = Date.now();
|
||||
const manager = new LiveStateManager(
|
||||
makeClickHouse(),
|
||||
redis as never,
|
||||
{
|
||||
options: 10000,
|
||||
nbbo: 10000,
|
||||
equities: 10000,
|
||||
"equity-quotes": 10000,
|
||||
"equity-joins": 10000,
|
||||
flow: 2,
|
||||
"smart-money": 10000,
|
||||
"classifier-hits": 10000,
|
||||
alerts: 10000,
|
||||
"inferred-dark": 10000
|
||||
}
|
||||
);
|
||||
const manager = new LiveStateManager(makeClickHouse(), redis as never, {
|
||||
options: 10000,
|
||||
nbbo: 10000,
|
||||
equities: 10000,
|
||||
"equity-quotes": 10000,
|
||||
"equity-joins": 10000,
|
||||
flow: 2,
|
||||
"smart-money": 10000,
|
||||
"classifier-hits": 10000,
|
||||
alerts: 10000,
|
||||
"inferred-dark": 10000
|
||||
});
|
||||
|
||||
await manager.ingest("flow", {
|
||||
source_ts: now,
|
||||
|
|
@ -503,18 +497,15 @@ describe("LiveStateManager", () => {
|
|||
manager.getSnapshot({ channel: "flow" })
|
||||
]);
|
||||
|
||||
expect((optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
|
||||
"opt-fresh",
|
||||
"opt-stale"
|
||||
]);
|
||||
expect((nbboSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
|
||||
"nbbo-fresh",
|
||||
"nbbo-stale"
|
||||
]);
|
||||
expect((equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
|
||||
"eq-fresh",
|
||||
"eq-stale"
|
||||
]);
|
||||
expect(
|
||||
(optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
|
||||
).toEqual(["opt-fresh", "opt-stale"]);
|
||||
expect(
|
||||
(nbboSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
|
||||
).toEqual(["nbbo-fresh", "nbbo-stale"]);
|
||||
expect(
|
||||
(equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
|
||||
).toEqual(["eq-fresh", "eq-stale"]);
|
||||
expect((flowSnapshot.items as Array<{ id: string }>).map((item) => item.id)).toEqual([
|
||||
"flow-fresh",
|
||||
"flow-stale"
|
||||
|
|
@ -699,10 +690,9 @@ describe("LiveStateManager", () => {
|
|||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
});
|
||||
|
||||
expect((snapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id).slice(0, 2)).toEqual([
|
||||
"opt-hot",
|
||||
"opt-backfill"
|
||||
]);
|
||||
expect(
|
||||
(snapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id).slice(0, 2)
|
||||
).toEqual(["opt-hot", "opt-backfill"]);
|
||||
});
|
||||
|
||||
it("seeds scoped equity snapshots from clickhouse rows older than 24h", async () => {
|
||||
|
|
@ -806,12 +796,12 @@ describe("LiveStateManager", () => {
|
|||
manager.getSnapshot({ channel: "flow" })
|
||||
]);
|
||||
|
||||
expect((optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
|
||||
"opt-retained"
|
||||
]);
|
||||
expect((equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
|
||||
"eq-retained"
|
||||
]);
|
||||
expect(
|
||||
(optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
|
||||
).toEqual(["opt-retained"]);
|
||||
expect(
|
||||
(equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
|
||||
).toEqual(["eq-retained"]);
|
||||
expect((flowSnapshot.items as Array<{ id: string }>).map((item) => item.id)).toEqual([
|
||||
"flow-retained"
|
||||
]);
|
||||
|
|
@ -1047,7 +1037,10 @@ describe("LiveStateManager", () => {
|
|||
});
|
||||
|
||||
it("tracks generic cache and scoped clickhouse snapshot sources separately", async () => {
|
||||
const manager = new LiveStateManager(makeClickHouse(() => []), null);
|
||||
const manager = new LiveStateManager(
|
||||
makeClickHouse(() => []),
|
||||
null
|
||||
);
|
||||
const now = Date.now();
|
||||
|
||||
await manager.ingest("options", {
|
||||
|
|
@ -1075,7 +1068,10 @@ describe("LiveStateManager", () => {
|
|||
});
|
||||
|
||||
it("keeps backend channel health healthy when a scoped query is quiet", async () => {
|
||||
const manager = new LiveStateManager(makeClickHouse(() => []), null);
|
||||
const manager = new LiveStateManager(
|
||||
makeClickHouse(() => []),
|
||||
null
|
||||
);
|
||||
const now = Date.now();
|
||||
|
||||
await manager.ingest("options", {
|
||||
|
|
@ -1098,7 +1094,9 @@ describe("LiveStateManager", () => {
|
|||
|
||||
expect(quietSnapshot.items).toEqual([]);
|
||||
expect(manager.getHotChannelHealth().options.healthy).toBe(true);
|
||||
expect(manager.getStatsSnapshot().freshnessAgeMsByKey[HOT_LIVE_REDIS_KEYS.options]).toBeLessThanOrEqual(50);
|
||||
expect(
|
||||
manager.getStatsSnapshot().freshnessAgeMsByKey[HOT_LIVE_REDIS_KEYS.options]
|
||||
).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
it("exposes freshness helper for feed status", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue