Implement scoped live 24h feed visibility

This commit is contained in:
dirtydishes 2026-05-04 05:52:38 -04:00
parent f28c8e641f
commit 48b0d980a6
11 changed files with 547 additions and 49 deletions

View file

@ -384,6 +384,59 @@ input {
color: #ffd89a;
}
.instrument-focus-chip {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
max-width: min(360px, 32vw);
padding: 6px 8px 6px 10px;
border: 1px solid rgba(255, 216, 154, 0.34);
border-radius: 8px;
background: rgba(245, 166, 35, 0.08);
color: #ffe2aa;
font-family: var(--font-mono), monospace;
font-size: 0.72rem;
font-weight: 700;
}
.instrument-focus-chip span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.instrument-focus-chip button,
.instrument-cell-button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.instrument-focus-chip button {
padding: 4px 6px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.62rem;
}
.instrument-cell-button {
padding: 0;
text-align: inherit;
text-decoration: underline;
text-decoration-color: rgba(255, 216, 154, 0.36);
text-underline-offset: 3px;
}
.instrument-cell-button:hover,
.instrument-cell-button:focus-visible {
color: #ffd89a;
outline: none;
}
.pause-button {
padding: 7px 10px;
font-size: 0.66rem;

View file

@ -73,6 +73,32 @@ describe("live manifest", () => {
expect(optionsSubscription?.filters).toBe(filters);
});
it("includes scoped option and equity subscriptions", () => {
const manifest = getLiveManifest(
"/tape",
"AAPL",
60000,
buildDefaultFlowFilters(),
{
underlying_ids: ["AAPL"],
option_contract_id: "AAPL-2025-01-17-200-C"
},
{ underlying_ids: ["AAPL"] }
);
const optionsSubscription = manifest.find(
(subscription): subscription is Extract<(typeof manifest)[number], { channel: "options" }> =>
subscription.channel === "options"
);
const equitiesSubscription = manifest.find(
(subscription): subscription is Extract<(typeof manifest)[number], { channel: "equities" }> =>
subscription.channel === "equities"
);
expect(optionsSubscription?.underlying_ids).toEqual(["AAPL"]);
expect(optionsSubscription?.option_contract_id).toBe("AAPL-2025-01-17-200-C");
expect(equitiesSubscription?.underlying_ids).toEqual(["AAPL"]);
});
});
describe("live tape pausable helpers", () => {

View file

@ -13,6 +13,7 @@ import {
useState,
type CSSProperties,
type Dispatch,
type MouseEvent as ReactMouseEvent,
type ReactNode,
type SetStateAction
} from "react";
@ -124,6 +125,11 @@ type ChartCandle = {
close: number;
};
type SelectedInstrument =
| null
| { kind: "equity"; underlyingId: string }
| { kind: "option-contract"; contractId: string; underlyingId: string };
const formatIntervalLabel = (intervalMs: number): string => {
const match = CANDLE_INTERVALS.find((interval) => interval.ms === intervalMs);
if (match) {
@ -2247,6 +2253,15 @@ const appendOptionFlowFilters = (params: URLSearchParams, filters: OptionFlowFil
}
};
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(","));
}
if (subscription.channel === "options" && subscription.option_contract_id) {
params.set("option_contract_id", subscription.option_contract_id);
}
};
const dedupeLiveSubscriptions = (subscriptions: LiveSubscription[]): LiveSubscription[] => {
const seen = new Set<string>();
return subscriptions.filter((subscription) => {
@ -2263,9 +2278,11 @@ export const getLiveManifest = (
pathname: string,
chartTicker: string,
chartIntervalMs: number,
flowFilters: OptionFlowFilters
flowFilters: OptionFlowFilters,
optionScope?: Pick<Extract<LiveSubscription, { channel: "options" }>, "underlying_ids" | "option_contract_id">,
equityScope?: Pick<Extract<LiveSubscription, { channel: "equities" }>, "underlying_ids">
): LiveSubscription[] => {
const baselineSubs: LiveSubscription[] = [{ channel: "options", filters: flowFilters }];
const baselineSubs: LiveSubscription[] = [{ channel: "options", filters: flowFilters, ...optionScope }];
const chartSubs: LiveSubscription[] = [
{ channel: "equity-candles", underlying_id: chartTicker, interval_ms: chartIntervalMs },
{ channel: "equity-overlay", underlying_id: chartTicker }
@ -2274,9 +2291,9 @@ export const getLiveManifest = (
if (pathname === "/tape") {
return dedupeLiveSubscriptions([
...baselineSubs,
{ channel: "options", filters: flowFilters },
{ channel: "options", filters: flowFilters, ...optionScope },
{ channel: "nbbo" },
{ channel: "equities" },
{ channel: "equities", ...equityScope },
{ channel: "flow", filters: flowFilters },
{ channel: "classifier-hits" }
]);
@ -2306,7 +2323,7 @@ export const getLiveManifest = (
return dedupeLiveSubscriptions([
...baselineSubs,
{ channel: "equities" },
{ channel: "equities", ...equityScope },
{ channel: "flow" },
{ channel: "alerts" },
{ channel: "classifier-hits" },
@ -2320,7 +2337,9 @@ const useLiveSession = (
pathname: string,
chartTicker: string,
chartIntervalMs: number,
flowFilters: OptionFlowFilters
flowFilters: OptionFlowFilters,
optionScope?: Pick<Extract<LiveSubscription, { channel: "options" }>, "underlying_ids" | "option_contract_id">,
equityScope?: Pick<Extract<LiveSubscription, { channel: "equities" }>, "underlying_ids">
): LiveSessionState => {
const [status, setStatus] = useState<WsStatus>(enabled ? "connecting" : "disconnected");
const [connectedAt, setConnectedAt] = useState<number | null>(null);
@ -2350,8 +2369,8 @@ const useLiveSession = (
const subscribedKeysRef = useRef<Set<string>>(new Set());
const subscribedMapRef = useRef<Map<string, LiveSubscription>>(new Map());
const manifest = useMemo(
() => getLiveManifest(pathname, chartTicker.toUpperCase(), chartIntervalMs, flowFilters),
[pathname, chartTicker, chartIntervalMs, flowFilters]
() => getLiveManifest(pathname, chartTicker.toUpperCase(), chartIntervalMs, flowFilters, optionScope, equityScope),
[pathname, chartTicker, chartIntervalMs, flowFilters, optionScope, equityScope]
);
useEffect(() => {
@ -2616,6 +2635,42 @@ const useLiveSession = (
const currentKeys = subscribedKeysRef.current;
const toSubscribe = manifest.filter((sub) => !currentKeys.has(getLiveSubscriptionKey(sub)));
const removedKeys = Array.from(currentKeys).filter((key) => !nextKeys.has(key));
const resetScopedChannels = new Set(
[...removedKeys, ...toSubscribe.map(getLiveSubscriptionKey)]
.map((key) => subscribedMapRef.current.get(key) ?? nextMap.get(key) ?? null)
.filter((sub): sub is LiveSubscription => sub !== null)
.map((sub) => sub.channel)
.filter((channel) => channel === "options" || channel === "equities")
);
if (resetScopedChannels.has("options")) {
setOptions([]);
}
if (resetScopedChannels.has("equities")) {
setEquities([]);
}
if (resetScopedChannels.size > 0) {
setHistoryCursors((current) => {
const next = { ...current };
for (const key of [...removedKeys, ...toSubscribe.map(getLiveSubscriptionKey)]) {
delete next[key];
}
return next;
});
setHistoryLoading((current) => {
const next = { ...current };
for (const key of [...removedKeys, ...toSubscribe.map(getLiveSubscriptionKey)]) {
delete next[key];
}
return next;
});
setHistoryErrors((current) => {
const next = { ...current };
for (const key of [...removedKeys, ...toSubscribe.map(getLiveSubscriptionKey)]) {
delete next[key];
}
return next;
});
}
if (removedKeys.length > 0) {
const removedSubs = removedKeys
@ -2660,6 +2715,7 @@ const useLiveSession = (
if (subscription.channel === "options" || subscription.channel === "flow") {
appendOptionFlowFilters(params, subscription.filters);
}
appendLiveScopeParams(params, subscription);
const response = await fetch(buildApiUrl(`${endpoint}?${params.toString()}`));
if (!response.ok) {
const detail = await readErrorDetail(response);
@ -3981,6 +4037,7 @@ const useTerminalState = () => {
const [selectedAlert, setSelectedAlert] = useState<AlertEvent | null>(null);
const [selectedDarkEvent, setSelectedDarkEvent] = useState<InferredDarkEvent | null>(null);
const [selectedClassifierHit, setSelectedClassifierHit] = useState<ClassifierHitEvent | null>(null);
const [selectedInstrument, setSelectedInstrument] = useState<SelectedInstrument>(null);
const [filterInput, setFilterInput] = useState<string>("");
const [flowFilters, setFlowFilters] = useState<OptionFlowFilters>(() => buildDefaultFlowFilters());
const [chartIntervalMs, setChartIntervalMs] = useState<number>(CANDLE_INTERVALS[0].ms);
@ -3992,20 +4049,52 @@ const useTerminalState = () => {
return Array.from(new Set(parts));
}, [filterInput]);
const tickerSet = useMemo(() => new Set(activeTickers), [activeTickers]);
const chartTicker = useMemo(() => activeTickers[0] ?? "SPY", [activeTickers]);
const instrumentUnderlying = selectedInstrument?.underlyingId.toUpperCase() ?? null;
const optionScope = useMemo(
() => ({
underlying_ids: activeTickers.length > 0 ? activeTickers : instrumentUnderlying ? [instrumentUnderlying] : undefined,
option_contract_id:
selectedInstrument?.kind === "option-contract" ? selectedInstrument.contractId : undefined
}),
[activeTickers, instrumentUnderlying, selectedInstrument]
);
const equityScope = useMemo(
() => ({
underlying_ids: activeTickers.length > 0 ? activeTickers : instrumentUnderlying ? [instrumentUnderlying] : undefined
}),
[activeTickers, instrumentUnderlying]
);
const chartTicker = useMemo(
() => instrumentUnderlying ?? activeTickers[0] ?? "SPY",
[activeTickers, instrumentUnderlying]
);
const selectedInstrumentLabel = useMemo(() => {
if (!selectedInstrument) {
return null;
}
if (selectedInstrument.kind === "equity") {
return `Equity: ${selectedInstrument.underlyingId}`;
}
const display = formatOptionContractLabel(selectedInstrument.contractId);
return display
? `Contract: ${display.ticker} ${display.expiration} ${display.strike}`
: `Contract: ${selectedInstrument.contractId}`;
}, [selectedInstrument]);
const liveSession = useLiveSession(
mode === "live",
pathname,
chartTicker,
chartIntervalMs,
flowFilters
flowFilters,
optionScope,
equityScope
);
const equitiesLiveSubscriptionActive = useMemo(
() =>
getLiveManifest(pathname, chartTicker.toUpperCase(), chartIntervalMs, flowFilters).some(
getLiveManifest(pathname, chartTicker.toUpperCase(), chartIntervalMs, flowFilters, optionScope, equityScope).some(
(sub) => sub.channel === "equities"
),
[pathname, chartTicker, chartIntervalMs, flowFilters]
[pathname, chartTicker, chartIntervalMs, flowFilters, optionScope, equityScope]
);
const handleReplaySource = useCallback((value: string | null) => {
@ -4665,19 +4754,31 @@ const useTerminalState = () => {
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 true;
return (
!instrumentUnderlying ||
extractUnderlying(normalizeContractId(print.option_contract_id)) === instrumentUnderlying
);
}
return matchesTicker(extractUnderlying(normalizeContractId(print.option_contract_id)));
});
}, [flowFilters, optionsFeed.items, matchesTicker, tickerSet]);
}, [flowFilters, optionsFeed.items, matchesTicker, tickerSet, selectedInstrument, instrumentUnderlying]);
const filteredEquities = useMemo(() => {
if (tickerSet.size === 0) {
if (instrumentUnderlying) {
return equitiesFeed.items.filter((print) => print.underlying_id.toUpperCase() === instrumentUnderlying);
}
return equitiesFeed.items;
}
return equitiesFeed.items.filter((print) => matchesTicker(print.underlying_id));
}, [equitiesFeed.items, matchesTicker, tickerSet]);
}, [equitiesFeed.items, matchesTicker, tickerSet, instrumentUnderlying]);
const equitiesSilentWarning = shouldShowEquitiesSilentFeedWarning({
wsStatus: liveSession.status,
@ -5000,6 +5101,9 @@ const useTerminalState = () => {
setSelectedDarkEvent,
selectedClassifierHit,
setSelectedClassifierHit,
selectedInstrument,
setSelectedInstrument,
selectedInstrumentLabel,
filterInput,
setFilterInput,
flowFilters,
@ -5473,6 +5577,15 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
const spot = print.execution_underlying_spot;
const iv = print.execution_iv;
const decor = state.classifierDecorByOptionTraceId.get(print.trace_id);
const underlyingId = (print.underlying_id ?? parsed?.root ?? extractUnderlying(contractId)).toUpperCase();
const focusContract = (event: ReactMouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
state.setSelectedInstrument({
kind: "option-contract",
contractId,
underlyingId
});
};
const commonProps = {
className: `data-table-row data-table-row-button data-table-row-classified data-table-row-options${decor ? ` is-classified classifier-${decor.tone}` : ""}`,
style: decor ? ({ "--classifier-intensity": decor.intensity } as CSSProperties) : undefined
@ -5480,10 +5593,26 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
const cells = (
<>
<span className="data-table-cell data-table-cell-number">{formatTime(print.ts)}</span>
<span className="data-table-cell">{contractDisplay?.ticker ?? parsed?.root ?? formatContractLabel(contractId)}</span>
<span className="data-table-cell">{contractDisplay?.expiration ?? parsed?.expiry ?? "--"}</span>
<span className="data-table-cell data-table-cell-number">{contractDisplay?.strike.replace(/[CP]$/, "") ?? "--"}</span>
<span className="data-table-cell">{parsed?.right ?? contractDisplay?.strike.slice(-1) ?? "--"}</span>
<span className="data-table-cell">
<button className="instrument-cell-button" type="button" onClick={focusContract}>
{contractDisplay?.ticker ?? parsed?.root ?? formatContractLabel(contractId)}
</button>
</span>
<span className="data-table-cell">
<button className="instrument-cell-button" type="button" onClick={focusContract}>
{contractDisplay?.expiration ?? parsed?.expiry ?? "--"}
</button>
</span>
<span className="data-table-cell data-table-cell-number">
<button className="instrument-cell-button" type="button" onClick={focusContract}>
{contractDisplay?.strike.replace(/[CP]$/, "") ?? "--"}
</button>
</span>
<span className="data-table-cell">
<button className="instrument-cell-button" type="button" onClick={focusContract}>
{parsed?.right ?? contractDisplay?.strike.slice(-1) ?? "--"}
</button>
</span>
<span className="data-table-cell data-table-cell-number">{typeof spot === "number" ? formatPrice(spot) : "--"}</span>
<span className="data-table-cell data-table-cell-number">
{formatSize(print.size)}@{formatPrice(print.price)}_{nbboSide ?? "--"}
@ -5598,7 +5727,20 @@ const EquitiesPane = ({ limit }: EquitiesPaneProps) => {
{virtual.visibleItems.map((print) => (
<div className="data-table-row data-table-row-equities" key={`${print.trace_id}-${print.seq}`}>
<span className="data-table-cell data-table-cell-number">{formatTime(print.ts)}</span>
<span className="data-table-cell">{print.underlying_id}</span>
<span className="data-table-cell">
<button
className="instrument-cell-button"
type="button"
onClick={() =>
state.setSelectedInstrument({
kind: "equity",
underlyingId: print.underlying_id.toUpperCase()
})
}
>
{print.underlying_id}
</button>
</span>
<span className="data-table-cell data-table-cell-number">${formatPrice(print.price)}</span>
<span className="data-table-cell data-table-cell-number">{formatSize(print.size)}x</span>
<span className="data-table-cell">{print.exchange}</span>
@ -6237,6 +6379,14 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
>
Clear
</button>
{state.selectedInstrumentLabel ? (
<span className="instrument-focus-chip">
<span>{state.selectedInstrumentLabel}</span>
<button type="button" onClick={() => state.setSelectedInstrument(null)}>
Clear
</button>
</span>
) : null}
</div>
<div className="terminal-topbar-mode">
<button