Fix contract-focused options tape hydration
This commit is contained in:
parent
73e25ddf70
commit
b73e62bdba
8 changed files with 657 additions and 171 deletions
|
|
@ -4,19 +4,23 @@ import {
|
|||
NAV_ITEMS,
|
||||
appendHistoryTail,
|
||||
buildDefaultFlowFilters,
|
||||
buildOptionTapeQueryParams,
|
||||
classifierToneForFamily,
|
||||
composeTapeItems,
|
||||
deriveAlertDirection,
|
||||
countActiveFlowFilterGroups,
|
||||
filterOptionTapeItems,
|
||||
findAnchorRestoreIndex,
|
||||
formatCompactUsd,
|
||||
formatOptionContractLabel,
|
||||
flushPausableTapeData,
|
||||
getEffectiveOptionPrintFilters,
|
||||
getAlertWindowAnchorTs,
|
||||
getHotChannelFeedStatus,
|
||||
getScopedLiveAutoHydrationChannels,
|
||||
getLiveHistoryRetentionCap,
|
||||
getOptionTableSnapshot,
|
||||
getOptionScope,
|
||||
getLiveFeedStatus,
|
||||
getLiveManifest,
|
||||
getRouteFeatures,
|
||||
|
|
@ -30,6 +34,7 @@ import {
|
|||
shouldIncludeEquitiesForDarkUnderlyingFallback,
|
||||
shouldShowEquitiesSilentFeedWarning,
|
||||
selectPrimaryClassifierHit,
|
||||
shouldClearOptionFocusSeed,
|
||||
smartMoneyProfileLabel,
|
||||
smartMoneyToneForProfile,
|
||||
statusLabel,
|
||||
|
|
@ -42,6 +47,25 @@ const makeItem = (traceId: string, seq: number, ts: number) => ({
|
|||
ts
|
||||
});
|
||||
|
||||
const makeOptionPrint = (overrides: Record<string, unknown> = {}) =>
|
||||
({
|
||||
trace_id: "opt-1",
|
||||
seq: 1,
|
||||
ts: 1_000,
|
||||
source_ts: 1_000,
|
||||
ingest_ts: 1_001,
|
||||
option_contract_id: "AAPL-2025-01-17-200-C",
|
||||
underlying_id: "AAPL",
|
||||
option_type: "call",
|
||||
nbbo_side: "A",
|
||||
notional: 250_000,
|
||||
signal_pass: true,
|
||||
price: 1,
|
||||
size: 10,
|
||||
exchange: "X",
|
||||
...overrides
|
||||
}) as any;
|
||||
|
||||
const makeAlert = (overrides: Record<string, unknown> = {}) =>
|
||||
({
|
||||
trace_id: "alert-1",
|
||||
|
|
@ -125,6 +149,31 @@ describe("live manifest", () => {
|
|||
expect(equitiesSubscription?.underlying_ids).toEqual(["AAPL"]);
|
||||
});
|
||||
|
||||
it("drops option-print filters for contract-focused options subscriptions but keeps flow filters", () => {
|
||||
const filters = {
|
||||
...buildDefaultFlowFilters(),
|
||||
minNotional: 500_000,
|
||||
optionTypes: ["put"] as const
|
||||
};
|
||||
const manifest = getLiveManifest(
|
||||
"/tape",
|
||||
"AAPL",
|
||||
60000,
|
||||
filters,
|
||||
{
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
},
|
||||
{ underlying_ids: ["AAPL"] },
|
||||
undefined
|
||||
);
|
||||
const optionsSubscription = manifest.find((subscription) => subscription.channel === "options");
|
||||
const flowSubscription = manifest.find((subscription) => subscription.channel === "flow");
|
||||
|
||||
expect(optionsSubscription?.filters).toBeUndefined();
|
||||
expect(flowSubscription?.filters).toBe(filters);
|
||||
});
|
||||
|
||||
it("scopes /signals subscriptions to signals channels only", () => {
|
||||
const channels = getLiveManifest("/signals", "SPY", 60000, buildDefaultFlowFilters()).map(
|
||||
(subscription) => subscription.channel
|
||||
|
|
@ -154,6 +203,130 @@ describe("live manifest", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("contract-focused option helpers", () => {
|
||||
it("uses the focused contract underlying for option scope even when ticker input differs", () => {
|
||||
expect(
|
||||
getOptionScope(["MSFT"], "AAPL", {
|
||||
kind: "option-contract",
|
||||
contractId: "AAPL-2025-01-17-200-C",
|
||||
underlyingId: "AAPL"
|
||||
})
|
||||
).toEqual({
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores broad flow filters for focused contract options", () => {
|
||||
const filters = {
|
||||
...buildDefaultFlowFilters(),
|
||||
minNotional: 500_000
|
||||
};
|
||||
const items = [
|
||||
makeOptionPrint({
|
||||
trace_id: "focused-low",
|
||||
option_contract_id: "AAPL-2025-01-17-200-C",
|
||||
notional: 100_000,
|
||||
signal_pass: false
|
||||
}),
|
||||
makeOptionPrint({
|
||||
trace_id: "focused-high",
|
||||
seq: 2,
|
||||
ts: 2_000,
|
||||
option_contract_id: "AAPL-2025-01-17-200-C",
|
||||
notional: 750_000
|
||||
}),
|
||||
makeOptionPrint({
|
||||
trace_id: "other-contract",
|
||||
seq: 3,
|
||||
ts: 3_000,
|
||||
option_contract_id: "MSFT-2025-01-17-300-C",
|
||||
underlying_id: "MSFT",
|
||||
notional: 900_000
|
||||
})
|
||||
];
|
||||
|
||||
expect(
|
||||
filterOptionTapeItems(
|
||||
items,
|
||||
getEffectiveOptionPrintFilters(filters, true),
|
||||
{
|
||||
kind: "option-contract",
|
||||
contractId: "AAPL-2025-01-17-200-C",
|
||||
underlyingId: "AAPL"
|
||||
},
|
||||
new Set(["MSFT"]),
|
||||
"AAPL"
|
||||
).map((item) => item.trace_id)
|
||||
).toEqual(["focused-low", "focused-high"]);
|
||||
});
|
||||
|
||||
it("includes option_contract_id and drops broad filters in focused replay query params", () => {
|
||||
const filters = {
|
||||
...buildDefaultFlowFilters(),
|
||||
minNotional: 500_000,
|
||||
optionTypes: ["put"] as const
|
||||
};
|
||||
|
||||
expect(
|
||||
buildOptionTapeQueryParams(getEffectiveOptionPrintFilters(filters, true), {
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
})
|
||||
).toEqual({
|
||||
underlying_ids: "AAPL",
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the focus seed until the matching scoped subscription has loaded it", () => {
|
||||
const seedItem = makeOptionPrint({
|
||||
trace_id: "focused-seed",
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
});
|
||||
const seed = {
|
||||
scopeKey: "option-contract:AAPL-2025-01-17-200-C",
|
||||
subscriptionKey: getLiveSubscriptionKey({
|
||||
channel: "options",
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
}),
|
||||
items: [seedItem]
|
||||
};
|
||||
|
||||
expect(
|
||||
shouldClearOptionFocusSeed(
|
||||
seed,
|
||||
"option-contract:AAPL-2025-01-17-200-C",
|
||||
getLiveSubscriptionKey({
|
||||
channel: "options",
|
||||
filters: {
|
||||
...buildDefaultFlowFilters(),
|
||||
minNotional: 500_000
|
||||
},
|
||||
underlying_ids: ["AAPL"]
|
||||
}),
|
||||
[makeOptionPrint({ trace_id: "broad-old" })],
|
||||
[]
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldClearOptionFocusSeed(
|
||||
seed,
|
||||
"option-contract:AAPL-2025-01-17-200-C",
|
||||
getLiveSubscriptionKey({
|
||||
channel: "options",
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
}),
|
||||
[seedItem],
|
||||
[]
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("route feature map", () => {
|
||||
it("maps /tape to tape panes and dependencies", () => {
|
||||
const features = getRouteFeatures("/tape");
|
||||
|
|
|
|||
|
|
@ -350,9 +350,17 @@ type SelectedInstrument =
|
|||
|
||||
type TapeFocusSeed<T> = {
|
||||
scopeKey: string;
|
||||
subscriptionKey?: string;
|
||||
items: T[];
|
||||
};
|
||||
|
||||
type OptionScope = Pick<
|
||||
Extract<LiveSubscription, { channel: "options" }>,
|
||||
"underlying_ids" | "option_contract_id"
|
||||
>;
|
||||
|
||||
type EquityScope = Pick<Extract<LiveSubscription, { channel: "equities" }>, "underlying_ids">;
|
||||
|
||||
const formatIntervalLabel = (intervalMs: number): string => {
|
||||
const match = CANDLE_INTERVALS.find((interval) => interval.ms === intervalMs);
|
||||
if (match) {
|
||||
|
|
@ -1956,6 +1964,13 @@ const useTape = <T extends SortableItem & { seq: number }>(
|
|||
const replaySourceKey = config.replaySourceKey ?? null;
|
||||
const onReplaySourceKey = config.onReplaySourceKey;
|
||||
const queryParams = config.queryParams;
|
||||
const queryKey = useMemo(
|
||||
() =>
|
||||
JSON.stringify(
|
||||
Object.entries(queryParams ?? {}).sort(([left], [right]) => left.localeCompare(right))
|
||||
),
|
||||
[queryParams]
|
||||
);
|
||||
const hotWindowLimit = config.hotWindowLimit ?? LIVE_HOT_WINDOW;
|
||||
const [status, setStatus] = useState<WsStatus>("connecting");
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
|
|
@ -2046,7 +2061,7 @@ const useTape = <T extends SortableItem & { seq: number }>(
|
|||
pendingRef.current = [];
|
||||
pendingCountRef.current = 0;
|
||||
cancelFlush();
|
||||
}, [mode, replaySourceKey, cancelFlush]);
|
||||
}, [mode, replaySourceKey, queryKey, cancelFlush]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "replay" || !latestPath) {
|
||||
|
|
@ -2091,7 +2106,7 @@ const useTape = <T extends SortableItem & { seq: number }>(
|
|||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [mode, latestPath, getItemTs, replaySourceKey, queryParams]);
|
||||
}, [mode, latestPath, getItemTs, replaySourceKey, queryKey, queryParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "live" || config.liveEnabled === false) {
|
||||
|
|
@ -2242,9 +2257,14 @@ const useTape = <T extends SortableItem & { seq: number }>(
|
|||
}
|
||||
}
|
||||
|
||||
if (onReplaySourceKey && sourcePrefix && replaySourceNotifiedRef.current !== sourcePrefix) {
|
||||
replaySourceNotifiedRef.current = sourcePrefix;
|
||||
onReplaySourceKey(sourcePrefix);
|
||||
if (onReplaySourceKey) {
|
||||
if (sourcePrefix && replaySourceNotifiedRef.current !== sourcePrefix) {
|
||||
replaySourceNotifiedRef.current = sourcePrefix;
|
||||
onReplaySourceKey(sourcePrefix);
|
||||
} else if (!sourcePrefix && replaySourceNotifiedRef.current !== null) {
|
||||
replaySourceNotifiedRef.current = null;
|
||||
onReplaySourceKey(null);
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = sourcePrefix
|
||||
|
|
@ -2330,6 +2350,7 @@ const useTape = <T extends SortableItem & { seq: number }>(
|
|||
getReplayKey,
|
||||
replaySourceKey,
|
||||
onReplaySourceKey,
|
||||
queryKey,
|
||||
queryParams
|
||||
]);
|
||||
|
||||
|
|
@ -2784,6 +2805,99 @@ const appendOptionFlowFilters = (params: URLSearchParams, filters: OptionFlowFil
|
|||
}
|
||||
};
|
||||
|
||||
const appendOptionScopeParams = (
|
||||
params: URLSearchParams,
|
||||
optionScope: OptionScope | undefined
|
||||
): void => {
|
||||
if (optionScope?.underlying_ids?.length) {
|
||||
params.set("underlying_ids", optionScope.underlying_ids.join(","));
|
||||
}
|
||||
if (optionScope?.option_contract_id) {
|
||||
params.set("option_contract_id", optionScope.option_contract_id);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEffectiveOptionPrintFilters = (
|
||||
flowFilters: OptionFlowFilters,
|
||||
isOptionContractFocused: boolean
|
||||
): OptionFlowFilters | undefined => {
|
||||
return isOptionContractFocused ? undefined : flowFilters;
|
||||
};
|
||||
|
||||
export const getOptionScope = (
|
||||
activeTickers: string[],
|
||||
instrumentUnderlying: string | null,
|
||||
selectedInstrument: SelectedInstrument
|
||||
): OptionScope => ({
|
||||
underlying_ids:
|
||||
selectedInstrument?.kind === "option-contract"
|
||||
? instrumentUnderlying
|
||||
? [instrumentUnderlying]
|
||||
: undefined
|
||||
: activeTickers.length > 0
|
||||
? activeTickers
|
||||
: instrumentUnderlying
|
||||
? [instrumentUnderlying]
|
||||
: undefined,
|
||||
option_contract_id:
|
||||
selectedInstrument?.kind === "option-contract" ? selectedInstrument.contractId : undefined
|
||||
});
|
||||
|
||||
export const buildOptionTapeQueryParams = (
|
||||
filters: OptionFlowFilters | undefined,
|
||||
optionScope: OptionScope | undefined
|
||||
): Record<string, string | undefined> => {
|
||||
const params = new URLSearchParams();
|
||||
appendOptionFlowFilters(params, filters);
|
||||
appendOptionScopeParams(params, optionScope);
|
||||
return Object.fromEntries(params.entries());
|
||||
};
|
||||
|
||||
export const filterOptionTapeItems = (
|
||||
items: OptionPrint[],
|
||||
filters: OptionFlowFilters | undefined,
|
||||
selectedInstrument: SelectedInstrument,
|
||||
tickerSet: Set<string>,
|
||||
instrumentUnderlying: string | null
|
||||
): OptionPrint[] => {
|
||||
return items.filter((print) => {
|
||||
const contractId = normalizeContractId(print.option_contract_id);
|
||||
if (selectedInstrument?.kind === "option-contract") {
|
||||
return contractId === selectedInstrument.contractId;
|
||||
}
|
||||
if (!matchesOptionPrintFilters(print, filters)) {
|
||||
return false;
|
||||
}
|
||||
const underlying = extractUnderlying(contractId);
|
||||
if (tickerSet.size === 0) {
|
||||
return !instrumentUnderlying || underlying === instrumentUnderlying;
|
||||
}
|
||||
return Boolean(underlying) && tickerSet.has(underlying.toUpperCase());
|
||||
});
|
||||
};
|
||||
|
||||
export const shouldClearOptionFocusSeed = (
|
||||
seed: TapeFocusSeed<OptionPrint> | null,
|
||||
optionFocusScopeKey: string | null,
|
||||
currentOptionSubscriptionKey: string | null,
|
||||
liveItems: OptionPrint[],
|
||||
historyItems: OptionPrint[]
|
||||
): boolean => {
|
||||
if (!seed) {
|
||||
return false;
|
||||
}
|
||||
if (seed.scopeKey !== optionFocusScopeKey) {
|
||||
return true;
|
||||
}
|
||||
if (seed.subscriptionKey && seed.subscriptionKey !== currentOptionSubscriptionKey) {
|
||||
return false;
|
||||
}
|
||||
const liveKeys = new Set(
|
||||
composeTapeItems([], liveItems, historyItems).map((item) => getTapeItemKey(item))
|
||||
);
|
||||
return seed.items.every((item) => liveKeys.has(getTapeItemKey(item)));
|
||||
};
|
||||
|
||||
const appendLiveScopeParams = (params: URLSearchParams, subscription: LiveSubscription): void => {
|
||||
if ((subscription.channel === "options" || subscription.channel === "equities") && subscription.underlying_ids?.length) {
|
||||
params.set("underlying_ids", subscription.underlying_ids.join(","));
|
||||
|
|
@ -2810,8 +2924,9 @@ export const getLiveManifest = (
|
|||
chartTicker: string,
|
||||
chartIntervalMs: number,
|
||||
flowFilters: OptionFlowFilters,
|
||||
optionScope?: Pick<Extract<LiveSubscription, { channel: "options" }>, "underlying_ids" | "option_contract_id">,
|
||||
equityScope?: Pick<Extract<LiveSubscription, { channel: "equities" }>, "underlying_ids">
|
||||
optionScope?: OptionScope,
|
||||
equityScope?: EquityScope,
|
||||
optionPrintFilters?: OptionFlowFilters
|
||||
): LiveSubscription[] => {
|
||||
const features = getRouteFeatures(pathname);
|
||||
const subscriptions: LiveSubscription[] = [];
|
||||
|
|
@ -2819,7 +2934,10 @@ export const getLiveManifest = (
|
|||
if (features.options) {
|
||||
subscriptions.push({
|
||||
channel: "options",
|
||||
filters: flowFilters,
|
||||
filters:
|
||||
optionScope?.option_contract_id && optionPrintFilters === undefined
|
||||
? undefined
|
||||
: optionPrintFilters ?? flowFilters,
|
||||
...optionScope,
|
||||
snapshot_limit: LIVE_HOT_WINDOW_OPTIONS
|
||||
});
|
||||
|
|
@ -2868,11 +2986,7 @@ export const getLiveManifest = (
|
|||
const useLiveSession = (
|
||||
enabled: boolean,
|
||||
pathname: string,
|
||||
chartTicker: string,
|
||||
chartIntervalMs: number,
|
||||
flowFilters: OptionFlowFilters,
|
||||
optionScope?: Pick<Extract<LiveSubscription, { channel: "options" }>, "underlying_ids" | "option_contract_id">,
|
||||
equityScope?: Pick<Extract<LiveSubscription, { channel: "equities" }>, "underlying_ids">
|
||||
manifest: LiveSubscription[]
|
||||
): LiveSessionState => {
|
||||
const [status, setStatus] = useState<WsStatus>(enabled ? "connecting" : "disconnected");
|
||||
const [connectedAt, setConnectedAt] = useState<number | null>(null);
|
||||
|
|
@ -2938,11 +3052,6 @@ const useLiveSession = (
|
|||
const lastEventAtRef = useRef<number | null>(null);
|
||||
const subscribedKeysRef = useRef<Set<string>>(new Set());
|
||||
const subscribedMapRef = useRef<Map<string, LiveSubscription>>(new Map());
|
||||
const manifest = useMemo(
|
||||
() => getLiveManifest(pathname, chartTicker.toUpperCase(), chartIntervalMs, flowFilters, optionScope, equityScope),
|
||||
[pathname, chartTicker, chartIntervalMs, flowFilters, optionScope, equityScope]
|
||||
);
|
||||
|
||||
const replaceArrayState = <T,>(
|
||||
setter: Dispatch<SetStateAction<T[]>>,
|
||||
ref: { current: T[] },
|
||||
|
|
@ -4857,20 +4966,21 @@ const useTerminalState = () => {
|
|||
}, [filterInput]);
|
||||
const tickerSet = useMemo(() => new Set(activeTickers), [activeTickers]);
|
||||
const instrumentUnderlying = selectedInstrument?.underlyingId.toUpperCase() ?? null;
|
||||
const isOptionContractFocused = selectedInstrument?.kind === "option-contract";
|
||||
const focusedOptionContractId =
|
||||
selectedInstrument?.kind === "option-contract" ? selectedInstrument.contractId : null;
|
||||
const optionFocusScopeKey =
|
||||
selectedInstrument?.kind === "option-contract"
|
||||
? `option-contract:${selectedInstrument.contractId}`
|
||||
: null;
|
||||
focusedOptionContractId ? `option-contract:${focusedOptionContractId}` : null;
|
||||
const equityFocusScopeKey =
|
||||
selectedInstrument?.kind === "equity"
|
||||
? `equity:${selectedInstrument.underlyingId.toUpperCase()}`
|
||||
: null;
|
||||
const effectiveOptionPrintFilters = useMemo(
|
||||
() => getEffectiveOptionPrintFilters(flowFilters, isOptionContractFocused),
|
||||
[flowFilters, isOptionContractFocused]
|
||||
);
|
||||
const optionScope = useMemo(
|
||||
() => ({
|
||||
underlying_ids: activeTickers.length > 0 ? activeTickers : instrumentUnderlying ? [instrumentUnderlying] : undefined,
|
||||
option_contract_id:
|
||||
selectedInstrument?.kind === "option-contract" ? selectedInstrument.contractId : undefined
|
||||
}),
|
||||
() => getOptionScope(activeTickers, instrumentUnderlying, selectedInstrument),
|
||||
[activeTickers, instrumentUnderlying, selectedInstrument]
|
||||
);
|
||||
const equityScope = useMemo(
|
||||
|
|
@ -4895,14 +5005,39 @@ const useTerminalState = () => {
|
|||
? `Contract: ${display.ticker} ${display.expiration} ${display.strike}`
|
||||
: `Contract: ${selectedInstrument.contractId}`;
|
||||
}, [selectedInstrument]);
|
||||
const liveSession = useLiveSession(
|
||||
mode === "live",
|
||||
pathname,
|
||||
chartTicker,
|
||||
chartIntervalMs,
|
||||
flowFilters,
|
||||
optionScope,
|
||||
equityScope
|
||||
const liveManifest = useMemo(
|
||||
() =>
|
||||
getLiveManifest(
|
||||
pathname,
|
||||
chartTicker.toUpperCase(),
|
||||
chartIntervalMs,
|
||||
flowFilters,
|
||||
optionScope,
|
||||
equityScope,
|
||||
effectiveOptionPrintFilters
|
||||
),
|
||||
[
|
||||
pathname,
|
||||
chartTicker,
|
||||
chartIntervalMs,
|
||||
flowFilters,
|
||||
optionScope,
|
||||
equityScope,
|
||||
effectiveOptionPrintFilters
|
||||
]
|
||||
);
|
||||
const liveSession = useLiveSession(mode === "live", pathname, liveManifest);
|
||||
const currentOptionSubscription = useMemo(
|
||||
() =>
|
||||
liveManifest.find(
|
||||
(subscription): subscription is Extract<LiveSubscription, { channel: "options" }> =>
|
||||
subscription.channel === "options"
|
||||
) ?? null,
|
||||
[liveManifest]
|
||||
);
|
||||
const currentOptionSubscriptionKey = useMemo(
|
||||
() => (currentOptionSubscription ? getLiveSubscriptionKey(currentOptionSubscription) : null),
|
||||
[currentOptionSubscription]
|
||||
);
|
||||
const equitiesLiveSubscriptionActive = routeFeatures.equities;
|
||||
|
||||
|
|
@ -4966,18 +5101,8 @@ const useTerminalState = () => {
|
|||
);
|
||||
const disableReplayGrouping = useCallback(() => null, []);
|
||||
const optionQueryParams = useMemo<Record<string, string | undefined>>(
|
||||
() => ({
|
||||
view: flowFilters.view ?? "signal",
|
||||
security:
|
||||
flowFilters.securityTypes?.length === 1 ? flowFilters.securityTypes[0] : undefined,
|
||||
side: flowFilters.nbboSides?.length ? flowFilters.nbboSides.join(",") : undefined,
|
||||
type: flowFilters.optionTypes?.length ? flowFilters.optionTypes.join(",") : undefined,
|
||||
min_notional:
|
||||
typeof flowFilters.minNotional === "number"
|
||||
? String(flowFilters.minNotional)
|
||||
: undefined
|
||||
}),
|
||||
[flowFilters]
|
||||
() => buildOptionTapeQueryParams(effectiveOptionPrintFilters, optionScope),
|
||||
[effectiveOptionPrintFilters, optionScope]
|
||||
);
|
||||
|
||||
const options = useTape<OptionPrint>({
|
||||
|
|
@ -4992,9 +5117,10 @@ const useTerminalState = () => {
|
|||
pollMs: mode === "replay" ? 200 : undefined,
|
||||
captureScroll: optionsAnchor.capture,
|
||||
onNewItems: optionsScroll.onNewItems,
|
||||
getReplayKey: extractReplaySource,
|
||||
onReplaySourceKey: handleReplaySource,
|
||||
queryParams: optionQueryParams
|
||||
getReplayKey: isOptionContractFocused ? disableReplayGrouping : extractReplaySource,
|
||||
onReplaySourceKey: isOptionContractFocused ? undefined : handleReplaySource,
|
||||
queryParams: optionQueryParams,
|
||||
replaySourceKey: isOptionContractFocused ? null : replaySource
|
||||
});
|
||||
|
||||
const equities = useTape<EquityPrint>({
|
||||
|
|
@ -5010,6 +5136,12 @@ const useTerminalState = () => {
|
|||
onNewItems: equitiesScroll.onNewItems
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOptionContractFocused && replaySource !== null) {
|
||||
setReplaySource(null);
|
||||
}
|
||||
}, [isOptionContractFocused, replaySource]);
|
||||
|
||||
const equityJoins = useTape<EquityPrintJoin>({
|
||||
mode,
|
||||
liveEnabled: false,
|
||||
|
|
@ -5922,25 +6054,20 @@ const useTerminalState = () => {
|
|||
);
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
return optionsFeed.items.filter((print) => {
|
||||
if (!matchesOptionPrintFilters(print, flowFilters)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
selectedInstrument?.kind === "option-contract" &&
|
||||
normalizeContractId(print.option_contract_id) !== selectedInstrument.contractId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (tickerSet.size === 0) {
|
||||
return (
|
||||
!instrumentUnderlying ||
|
||||
extractUnderlying(normalizeContractId(print.option_contract_id)) === instrumentUnderlying
|
||||
);
|
||||
}
|
||||
return matchesTicker(extractUnderlying(normalizeContractId(print.option_contract_id)));
|
||||
});
|
||||
}, [flowFilters, optionsFeed.items, matchesTicker, tickerSet, selectedInstrument, instrumentUnderlying]);
|
||||
return filterOptionTapeItems(
|
||||
optionsFeed.items,
|
||||
effectiveOptionPrintFilters,
|
||||
selectedInstrument,
|
||||
tickerSet,
|
||||
instrumentUnderlying
|
||||
);
|
||||
}, [
|
||||
effectiveOptionPrintFilters,
|
||||
instrumentUnderlying,
|
||||
optionsFeed.items,
|
||||
selectedInstrument,
|
||||
tickerSet
|
||||
]);
|
||||
|
||||
const filteredEquities = useMemo(() => {
|
||||
if (tickerSet.size === 0) {
|
||||
|
|
@ -5956,16 +6083,24 @@ const useTerminalState = () => {
|
|||
if (!optionFocusSeed) {
|
||||
return;
|
||||
}
|
||||
if (optionFocusSeed.scopeKey !== optionFocusScopeKey) {
|
||||
setOptionFocusSeed(null);
|
||||
return;
|
||||
}
|
||||
const composedBaseItems = composeTapeItems([], liveOptions.liveItems ?? [], liveOptions.historyItems ?? []);
|
||||
const liveKeys = new Set(composedBaseItems.map((item) => getTapeItemKey(item)));
|
||||
if (optionFocusSeed.items.every((item) => liveKeys.has(getTapeItemKey(item)))) {
|
||||
if (
|
||||
shouldClearOptionFocusSeed(
|
||||
optionFocusSeed,
|
||||
optionFocusScopeKey,
|
||||
currentOptionSubscriptionKey,
|
||||
liveOptions.liveItems ?? [],
|
||||
liveOptions.historyItems ?? []
|
||||
)
|
||||
) {
|
||||
setOptionFocusSeed(null);
|
||||
}
|
||||
}, [liveOptions.historyItems, liveOptions.liveItems, optionFocusScopeKey, optionFocusSeed]);
|
||||
}, [
|
||||
currentOptionSubscriptionKey,
|
||||
liveOptions.historyItems,
|
||||
liveOptions.liveItems,
|
||||
optionFocusScopeKey,
|
||||
optionFocusSeed
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!equityFocusSeed) {
|
||||
|
|
@ -5988,15 +6123,21 @@ const useTerminalState = () => {
|
|||
const parsed = parseOptionContractId(contractId);
|
||||
const underlyingId = (print.underlying_id ?? parsed?.root ?? extractUnderlying(contractId)).toUpperCase();
|
||||
const scopeKey = `option-contract:${contractId}`;
|
||||
const subscriptionKey = getLiveSubscriptionKey({
|
||||
channel: "options",
|
||||
underlying_ids: [underlyingId],
|
||||
option_contract_id: contractId
|
||||
});
|
||||
const seedItems = composeTapeItems(
|
||||
[print],
|
||||
filteredOptions.filter((candidate) => normalizeContractId(candidate.option_contract_id) === contractId),
|
||||
[]
|
||||
);
|
||||
setOptionFocusSeed({ scopeKey, items: seedItems });
|
||||
setOptionFocusSeed({ scopeKey, subscriptionKey, items: seedItems });
|
||||
bumpTapeDebugMetric("focusSeedRowCount", seedItems.length);
|
||||
logTapeDebug("option focus seed captured", {
|
||||
contract_id: contractId,
|
||||
subscription_key: subscriptionKey,
|
||||
row_count: seedItems.length
|
||||
});
|
||||
setSelectedInstrument({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue