Expand options tape display and retention

- Add a dedicated hot-window limit for options prints
- Improve option contract and notional formatting in the tape
- Update docs, env sample, and tests
This commit is contained in:
dirtydishes 2026-04-29 00:10:37 -04:00
parent da942079f3
commit 9131e046cb
5 changed files with 235 additions and 18 deletions

View file

@ -58,6 +58,7 @@ COMPUTE_CONSUMER_RESET=false
NBBO_MAX_AGE_MS=1000 NBBO_MAX_AGE_MS=1000
NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000 NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000
NEXT_PUBLIC_LIVE_HOT_WINDOW=2000 NEXT_PUBLIC_LIVE_HOT_WINDOW=2000
NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS=25000
NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS=1200000 NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS=1200000
NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS=4000 NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS=4000
ROLLING_WINDOW_SIZE=50 ROLLING_WINDOW_SIZE=50

View file

@ -186,7 +186,8 @@ Default `smart-money` behavior:
### Web live retention ### Web live retention
- `NEXT_PUBLIC_LIVE_HOT_WINDOW` (frontend hot live window cap; default `2000`) - `NEXT_PUBLIC_LIVE_HOT_WINDOW` (frontend hot live window cap for non-options feeds; default `2000`)
- `NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS` (frontend hot live window cap for options prints; default `25000`)
- `NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS` (pinned evidence TTL; default `1200000`) - `NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS` (pinned evidence TTL; default `1200000`)
- `NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS` (pinned evidence cache guardrail; default `4000`) - `NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS` (pinned evidence cache guardrail; default `4000`)
- `NEXT_PUBLIC_FLOW_FILTER_PRESET` (`smart-money` | `balanced` | `all`, default `smart-money`) - `NEXT_PUBLIC_FLOW_FILTER_PRESET` (`smart-money` | `balanced` | `all`, default `smart-money`)
@ -211,6 +212,7 @@ Default `smart-money` behavior:
- Live retention uses a two-tier model: - Live retention uses a two-tier model:
- API/Redis maintain a bounded hot cache per live generic channel. - API/Redis maintain a bounded hot cache per live generic channel.
- UI keeps a bounded hot window for rendering performance around the signal view rather than raw noise. - UI keeps a bounded hot window for rendering performance around the signal view rather than raw noise.
- Options prints can use a deeper dedicated cap via `NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS` without raising every other feed.
- Alert/drawer evidence is pinned and hydrated by id/trace so details remain inspectable after hot-window eviction. - Alert/drawer evidence is pinned and hydrated by id/trace so details remain inspectable after hot-window eviction.
- Firehose-readiness strategy: - Firehose-readiness strategy:
- preserve raw ingest for storage/replay, - preserve raw ingest for storage/replay,

View file

@ -859,6 +859,12 @@ h3 {
font-weight: 600; font-weight: 600;
} }
.option-contract {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
}
.meta, .meta,
.drawer-row-meta, .drawer-row-meta,
.flow-meta { .flow-meta {
@ -868,6 +874,41 @@ h3 {
font-size: 0.76rem; font-size: 0.76rem;
} }
.notional-emphasis {
font-weight: 700;
letter-spacing: 0.01em;
color: #ffe08c;
}
.condition-chip {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 999px;
border: 1px solid var(--border);
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.condition-sweep {
border-color: rgba(37, 193, 122, 0.34);
color: #98f0c0;
background: var(--green-soft);
}
.condition-iso {
border-color: rgba(77, 163, 255, 0.34);
color: #bddcff;
background: var(--blue-soft);
}
.condition-neutral {
border-color: rgba(192, 200, 210, 0.28);
color: #d4dbe3;
background: rgba(192, 200, 210, 0.08);
}
.pill, .pill,
.drawer-chip, .drawer-chip,
.flag { .flag {

View file

@ -2,6 +2,8 @@ import { describe, expect, it } from "bun:test";
import { import {
buildDefaultFlowFilters, buildDefaultFlowFilters,
countActiveFlowFilterGroups, countActiveFlowFilterGroups,
formatCompactUsd,
formatOptionContractLabel,
flushPausableTapeData, flushPausableTapeData,
getLiveFeedStatus, getLiveFeedStatus,
nextFlowFilterPopoverState, nextFlowFilterPopoverState,
@ -51,6 +53,18 @@ describe("live tape pausable helpers", () => {
expect(state.visible.map((item) => item.trace_id)).toEqual(["a"]); expect(state.visible.map((item) => item.trace_id)).toEqual(["a"]);
}); });
it("applies custom retention limits when requested", () => {
const state = reducePausableTapeData(
{ visible: [], queued: [], seenKeys: new Set<string>(), dropped: 0 },
[makeItem("a", 1, 100), makeItem("b", 2, 200), makeItem("c", 3, 300)],
false,
2
);
expect(state.visible.map((item) => item.trace_id)).toEqual(["c", "b"]);
expect(state.visible).toHaveLength(2);
});
it("marks connected feeds stale once their freshest event ages past the threshold", () => { it("marks connected feeds stale once their freshest event ages past the threshold", () => {
expect(getLiveFeedStatus("connected", 1000, 500, 1400)).toBe("connected"); expect(getLiveFeedStatus("connected", 1000, 500, 1400)).toBe("connected");
expect(getLiveFeedStatus("connected", 1000, 500, 1601)).toBe("stale"); expect(getLiveFeedStatus("connected", 1000, 500, 1601)).toBe("stale");
@ -107,6 +121,43 @@ describe("live tape pausable helpers", () => {
}); });
}); });
describe("options display formatters", () => {
it("formats dashed option contracts as ticker strike expiry", () => {
expect(formatOptionContractLabel("SPY-2025-01-17-450-C")).toEqual({
ticker: "SPY",
strike: "450C",
expiration: "01-17-25"
});
});
it("formats OCC contracts as ticker strike expiry", () => {
expect(formatOptionContractLabel("AAPL250117P00150000")).toEqual({
ticker: "AAPL",
strike: "150P",
expiration: "01-17-25"
});
});
it("preserves decimal strikes and side suffix", () => {
expect(formatOptionContractLabel("QQQ-2025-01-17-509.5-C")).toEqual({
ticker: "QQQ",
strike: "509.5C",
expiration: "01-17-25"
});
});
it("returns null when contract parsing fails", () => {
expect(formatOptionContractLabel("not-a-contract")).toBeNull();
});
it("formats compact notional values", () => {
expect(formatCompactUsd(999)).toBe("999.00");
expect(formatCompactUsd(11_430)).toBe("11.4K");
expect(formatCompactUsd(1_250_000)).toBe("1.3M");
expect(formatCompactUsd(Number.NaN)).toBe("0.00");
});
});
describe("flow filter popup helpers", () => { describe("flow filter popup helpers", () => {
it("opens and closes the popup via toggle and dismiss actions", () => { it("opens and closes the popup via toggle and dismiss actions", () => {
expect(nextFlowFilterPopoverState(false, "toggle")).toBe(true); expect(nextFlowFilterPopoverState(false, "toggle")).toBe(true);

View file

@ -35,6 +35,7 @@ import type {
} from "@islandflow/types"; } from "@islandflow/types";
import { import {
getSubscriptionKey as getLiveSubscriptionKey, getSubscriptionKey as getLiveSubscriptionKey,
parseOptionContractId,
matchesFlowPacketFilters, matchesFlowPacketFilters,
matchesOptionPrintFilters matchesOptionPrintFilters
} from "@islandflow/types"; } from "@islandflow/types";
@ -57,6 +58,12 @@ const parseBoundedInt = (
}; };
const LIVE_HOT_WINDOW = parseBoundedInt(process.env.NEXT_PUBLIC_LIVE_HOT_WINDOW, 2000, 100, 100000); const LIVE_HOT_WINDOW = parseBoundedInt(process.env.NEXT_PUBLIC_LIVE_HOT_WINDOW, 2000, 100, 100000);
const LIVE_HOT_WINDOW_OPTIONS = parseBoundedInt(
process.env.NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS,
25000,
100,
100000
);
const LIVE_OPTIONS_STALE_MS = 15_000; const LIVE_OPTIONS_STALE_MS = 15_000;
const LIVE_NBBO_STALE_MS = 15_000; const LIVE_NBBO_STALE_MS = 15_000;
const LIVE_EQUITIES_STALE_MS = 15_000; const LIVE_EQUITIES_STALE_MS = 15_000;
@ -284,6 +291,12 @@ type PinnedEntry<T> = {
updatedAt: number; updatedAt: number;
}; };
type OptionContractDisplay = {
ticker: string;
strike: string;
expiration: string;
};
type RetentionMetricKey = type RetentionMetricKey =
| "hotWindowEvictions" | "hotWindowEvictions"
| "pinnedFetchMisses" | "pinnedFetchMisses"
@ -378,7 +391,8 @@ type PausableTapeData<T> = {
export const reducePausableTapeData = <T extends SortableItem>( export const reducePausableTapeData = <T extends SortableItem>(
current: PausableTapeData<T>, current: PausableTapeData<T>,
incoming: T[], incoming: T[],
paused: boolean paused: boolean,
retentionLimit = LIVE_HOT_WINDOW
): PausableTapeData<T> => { ): PausableTapeData<T> => {
if (incoming.length === 0) { if (incoming.length === 0) {
return current; return current;
@ -403,7 +417,7 @@ export const reducePausableTapeData = <T extends SortableItem>(
if (paused) { if (paused) {
return { return {
visible: current.visible, visible: current.visible,
queued: mergeNewest(unseen, current.queued, LIVE_HOT_WINDOW, (evicted) => queued: mergeNewest(unseen, current.queued, retentionLimit, (evicted) =>
incrementRetentionMetric("hotWindowEvictions", evicted) incrementRetentionMetric("hotWindowEvictions", evicted)
), ),
seenKeys: nextSeenKeys, seenKeys: nextSeenKeys,
@ -413,7 +427,7 @@ export const reducePausableTapeData = <T extends SortableItem>(
const nextBatch = current.queued.length > 0 ? [...current.queued, ...unseen] : unseen; const nextBatch = current.queued.length > 0 ? [...current.queued, ...unseen] : unseen;
return { return {
visible: mergeNewest(nextBatch, current.visible, LIVE_HOT_WINDOW, (evicted) => visible: mergeNewest(nextBatch, current.visible, retentionLimit, (evicted) =>
incrementRetentionMetric("hotWindowEvictions", evicted) incrementRetentionMetric("hotWindowEvictions", evicted)
), ),
queued: [], queued: [],
@ -423,14 +437,15 @@ export const reducePausableTapeData = <T extends SortableItem>(
}; };
export const flushPausableTapeData = <T extends SortableItem>( export const flushPausableTapeData = <T extends SortableItem>(
current: PausableTapeData<T> current: PausableTapeData<T>,
retentionLimit = LIVE_HOT_WINDOW
): PausableTapeData<T> => { ): PausableTapeData<T> => {
if (current.queued.length === 0) { if (current.queued.length === 0) {
return current.dropped === 0 ? current : { ...current, dropped: 0 }; return current.dropped === 0 ? current : { ...current, dropped: 0 };
} }
return { return {
visible: mergeNewest(current.queued, current.visible, LIVE_HOT_WINDOW, (evicted) => visible: mergeNewest(current.queued, current.visible, retentionLimit, (evicted) =>
incrementRetentionMetric("hotWindowEvictions", evicted) incrementRetentionMetric("hotWindowEvictions", evicted)
), ),
queued: [], queued: [],
@ -545,9 +560,74 @@ const formatUsd = (value: number): string => {
}); });
}; };
export const formatCompactUsd = (value: number): string => {
if (!Number.isFinite(value)) {
return "0.00";
}
const abs = Math.abs(value);
const sign = value < 0 ? "-" : "";
if (abs < 1_000) {
return formatUsd(value);
}
if (abs < 1_000_000) {
return `${sign}${(abs / 1_000).toFixed(1)}K`;
}
if (abs < 1_000_000_000) {
return `${sign}${(abs / 1_000_000).toFixed(1)}M`;
}
return `${sign}${(abs / 1_000_000_000).toFixed(1)}B`;
};
const normalizeContractId = (value: string): string => value.trim(); const normalizeContractId = (value: string): string => value.trim();
const formatStrike = (value: number): string => {
if (!Number.isFinite(value)) {
return "0";
}
if (Number.isInteger(value)) {
return value.toLocaleString(undefined, { maximumFractionDigits: 0 });
}
return value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 3 });
};
const formatExpiryShort = (value: string): string | null => {
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const [, year, month, day] = match;
return `${month}-${day}-${year.slice(2)}`;
};
export const formatOptionContractLabel = (value: string): OptionContractDisplay | null => {
const normalized = normalizeContractId(value);
if (!normalized) {
return null;
}
const parsed = parseOptionContractId(normalized);
if (!parsed) {
return null;
}
const expiration = formatExpiryShort(parsed.expiry);
if (!expiration) {
return null;
}
return {
ticker: parsed.root.toUpperCase(),
strike: `${formatStrike(parsed.strike)}${parsed.right}`,
expiration
};
};
const formatContractLabel = (value: string): string => { const formatContractLabel = (value: string): string => {
const parsed = formatOptionContractLabel(value);
if (parsed) {
return `${parsed.ticker} ${parsed.strike} ${parsed.expiration}`;
}
const normalized = normalizeContractId(value); const normalized = normalizeContractId(value);
if (!normalized) { if (!normalized) {
return "Unknown contract"; return "Unknown contract";
@ -1180,6 +1260,7 @@ type TapeConfig<T> = {
replaySourceKey?: string | null; replaySourceKey?: string | null;
onReplaySourceKey?: (key: string | null) => void; onReplaySourceKey?: (key: string | null) => void;
queryParams?: Record<string, string | null | undefined>; queryParams?: Record<string, string | null | undefined>;
hotWindowLimit?: number;
}; };
const useTape = <T extends SortableItem & { seq: number }>( const useTape = <T extends SortableItem & { seq: number }>(
@ -1193,6 +1274,7 @@ const useTape = <T extends SortableItem & { seq: number }>(
const replaySourceKey = config.replaySourceKey ?? null; const replaySourceKey = config.replaySourceKey ?? null;
const onReplaySourceKey = config.onReplaySourceKey; const onReplaySourceKey = config.onReplaySourceKey;
const queryParams = config.queryParams; const queryParams = config.queryParams;
const hotWindowLimit = config.hotWindowLimit ?? LIVE_HOT_WINDOW;
const [status, setStatus] = useState<WsStatus>("connecting"); const [status, setStatus] = useState<WsStatus>("connecting");
const [items, setItems] = useState<T[]>([]); const [items, setItems] = useState<T[]>([]);
const [lastUpdate, setLastUpdate] = useState<number | null>(null); const [lastUpdate, setLastUpdate] = useState<number | null>(null);
@ -1249,13 +1331,13 @@ const useTape = <T extends SortableItem & { seq: number }>(
} }
setItems((prev) => setItems((prev) =>
mergeNewest(buffered, prev, LIVE_HOT_WINDOW, (evicted) => mergeNewest(buffered, prev, hotWindowLimit, (evicted) =>
incrementRetentionMetric("hotWindowEvictions", evicted) incrementRetentionMetric("hotWindowEvictions", evicted)
) )
); );
setLastUpdate(Date.now()); setLastUpdate(Date.now());
}); });
}, [captureScroll, onNewItems]); }, [captureScroll, hotWindowLimit, onNewItems]);
const togglePause = useCallback(() => { const togglePause = useCallback(() => {
setPaused((prev) => { setPaused((prev) => {
@ -1605,6 +1687,7 @@ type PausableTapeViewConfig<T extends SortableItem & { seq: number }> = {
onNewItems?: (count: number) => void; onNewItems?: (count: number) => void;
captureScroll?: () => void; captureScroll?: () => void;
getItemTs?: (item: T) => number; getItemTs?: (item: T) => number;
retentionLimit?: number;
}; };
const usePausableTapeView = <T extends SortableItem & { seq: number }>( const usePausableTapeView = <T extends SortableItem & { seq: number }>(
@ -1632,7 +1715,12 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
} }
setData((current) => { setData((current) => {
const next = reducePausableTapeData(current, config.sourceItems, paused); const next = reducePausableTapeData(
current,
config.sourceItems,
paused,
config.retentionLimit ?? LIVE_HOT_WINDOW
);
if (next === current) { if (next === current) {
return current; return current;
} }
@ -1645,7 +1733,14 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
return next; return next;
}); });
}, [config.enabled, config.sourceItems, config.onNewItems, config.captureScroll, paused]); }, [
config.enabled,
config.sourceItems,
config.onNewItems,
config.captureScroll,
config.retentionLimit,
paused
]);
useEffect(() => { useEffect(() => {
if (!config.enabled || paused) { if (!config.enabled || paused) {
@ -1653,7 +1748,7 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
} }
setData((current) => { setData((current) => {
const next = flushPausableTapeData(current); const next = flushPausableTapeData(current, config.retentionLimit ?? LIVE_HOT_WINDOW);
if (next === current) { if (next === current) {
return current; return current;
} }
@ -1665,7 +1760,7 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
return next; return next;
}); });
}, [config.enabled, config.onNewItems, config.captureScroll, paused]); }, [config.captureScroll, config.enabled, config.onNewItems, config.retentionLimit, paused]);
const togglePause = useCallback(() => { const togglePause = useCallback(() => {
setPaused((current) => !current); setPaused((current) => !current);
@ -2094,7 +2189,8 @@ const useLiveSession = (
const mergeItems = <T extends SortableItem>( const mergeItems = <T extends SortableItem>(
setter: React.Dispatch<React.SetStateAction<T[]>>, setter: React.Dispatch<React.SetStateAction<T[]>>,
nextItems: T[] nextItems: T[],
retentionLimit = LIVE_HOT_WINDOW
) => { ) => {
setter((prev) => setter((prev) =>
message.op === "snapshot" message.op === "snapshot"
@ -2106,7 +2202,7 @@ const useLiveSession = (
) )
? prev ? prev
: (nextItems as T[]) : (nextItems as T[])
: mergeNewest(nextItems as T[], prev, LIVE_HOT_WINDOW, (evicted) => : mergeNewest(nextItems as T[], prev, retentionLimit, (evicted) =>
incrementRetentionMetric("hotWindowEvictions", evicted) incrementRetentionMetric("hotWindowEvictions", evicted)
) )
); );
@ -2114,7 +2210,7 @@ const useLiveSession = (
switch (subscription.channel) { switch (subscription.channel) {
case "options": case "options":
mergeItems(setOptions, items as OptionPrint[]); mergeItems(setOptions, items as OptionPrint[], LIVE_HOT_WINDOW_OPTIONS);
break; break;
case "nbbo": case "nbbo":
mergeItems(setNbbo, items as OptionNBBO[]); mergeItems(setNbbo, items as OptionNBBO[]);
@ -3532,6 +3628,7 @@ const useTerminalState = () => {
replayPath: "/replay/options", replayPath: "/replay/options",
latestPath: "/prints/options", latestPath: "/prints/options",
expectedType: "option-print", expectedType: "option-print",
hotWindowLimit: LIVE_HOT_WINDOW_OPTIONS,
batchSize: mode === "replay" ? 120 : undefined, batchSize: mode === "replay" ? 120 : undefined,
pollMs: mode === "replay" ? 200 : undefined, pollMs: mode === "replay" ? 200 : undefined,
captureScroll: optionsAnchor.capture, captureScroll: optionsAnchor.capture,
@ -3639,6 +3736,7 @@ const useTerminalState = () => {
sourceItems: liveSession.options, sourceItems: liveSession.options,
lastUpdate: liveSession.lastUpdate, lastUpdate: liveSession.lastUpdate,
freshnessMs: LIVE_OPTIONS_STALE_MS, freshnessMs: LIVE_OPTIONS_STALE_MS,
retentionLimit: LIVE_HOT_WINDOW_OPTIONS,
captureScroll: optionsAnchor.capture, captureScroll: optionsAnchor.capture,
onNewItems: optionsScroll.onNewItems onNewItems: optionsScroll.onNewItems
}); });
@ -4886,6 +4984,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
) : null} ) : null}
{virtual.visibleItems.map((print) => { {virtual.visibleItems.map((print) => {
const contractId = normalizeContractId(print.option_contract_id); const contractId = normalizeContractId(print.option_contract_id);
const contractDisplay = formatOptionContractLabel(contractId);
const quote = state.nbboMap.get(contractId); const quote = state.nbboMap.get(contractId);
const nbboAge = quote ? Math.abs(print.ts - quote.ts) : null; const nbboAge = quote ? Math.abs(print.ts - quote.ts) : null;
const nbboStale = nbboAge !== null && nbboAge > NBBO_MAX_AGE_MS_SAFE; const nbboStale = nbboAge !== null && nbboAge > NBBO_MAX_AGE_MS_SAFE;
@ -4896,13 +4995,36 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
return ( return (
<div className="row" key={`${print.trace_id}-${print.seq}`}> <div className="row" key={`${print.trace_id}-${print.seq}`}>
<div> <div>
<div className="contract">{formatContractLabel(contractId)}</div> <div className={`contract${contractDisplay ? " option-contract" : ""}`}>
{contractDisplay ? (
<>
<span>{contractDisplay.ticker}</span>
<span>{contractDisplay.strike}</span>
<span>{contractDisplay.expiration}</span>
</>
) : (
formatContractLabel(contractId)
)}
</div>
<div className="meta"> <div className="meta">
<span>${formatPrice(print.price)}</span> <span>${formatPrice(print.price)}</span>
<span>{formatSize(print.size)}x</span> <span>{formatSize(print.size)}x</span>
<span>{print.exchange}</span> <span>{print.exchange}</span>
<span>Notional ${formatUsd(notional)}</span> <span className="notional-emphasis">Notional ${formatCompactUsd(notional)}</span>
{print.conditions?.length ? <span>{print.conditions.join(", ")}</span> : null} {print.conditions?.map((condition) => {
const normalized = condition.toUpperCase();
const tone =
normalized === "SWEEP"
? "condition-sweep"
: normalized === "ISO"
? "condition-iso"
: "condition-neutral";
return (
<span className={`condition-chip ${tone}`} key={`${print.trace_id}-${condition}`}>
{normalized}
</span>
);
})}
</div> </div>
{quote ? ( {quote ? (
<div className="meta nbbo-meta"> <div className="meta nbbo-meta">