diff --git a/.env.example b/.env.example index 3b24669..5eb49e4 100644 --- a/.env.example +++ b/.env.example @@ -58,6 +58,7 @@ COMPUTE_CONSUMER_RESET=false NBBO_MAX_AGE_MS=1000 NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000 NEXT_PUBLIC_LIVE_HOT_WINDOW=2000 +NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS=25000 NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS=1200000 NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS=4000 ROLLING_WINDOW_SIZE=50 diff --git a/README.md b/README.md index f0a2b60..7627505 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,8 @@ Default `smart-money` behavior: ### 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_MAX_ITEMS` (pinned evidence cache guardrail; default `4000`) - `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: - 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. + - 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. - Firehose-readiness strategy: - preserve raw ingest for storage/replay, diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 0910153..af62e7a 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -859,6 +859,12 @@ h3 { font-weight: 600; } +.option-contract { + display: inline-flex; + flex-wrap: wrap; + gap: 8px; +} + .meta, .drawer-row-meta, .flow-meta { @@ -868,6 +874,41 @@ h3 { 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, .drawer-chip, .flag { diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 8d78abd..908f8bf 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "bun:test"; import { buildDefaultFlowFilters, countActiveFlowFilterGroups, + formatCompactUsd, + formatOptionContractLabel, flushPausableTapeData, getLiveFeedStatus, nextFlowFilterPopoverState, @@ -51,6 +53,18 @@ describe("live tape pausable helpers", () => { 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(), 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", () => { expect(getLiveFeedStatus("connected", 1000, 500, 1400)).toBe("connected"); 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", () => { it("opens and closes the popup via toggle and dismiss actions", () => { expect(nextFlowFilterPopoverState(false, "toggle")).toBe(true); diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 15bdbd8..4e6214f 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -35,6 +35,7 @@ import type { } from "@islandflow/types"; import { getSubscriptionKey as getLiveSubscriptionKey, + parseOptionContractId, matchesFlowPacketFilters, matchesOptionPrintFilters } 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_OPTIONS = parseBoundedInt( + process.env.NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS, + 25000, + 100, + 100000 +); const LIVE_OPTIONS_STALE_MS = 15_000; const LIVE_NBBO_STALE_MS = 15_000; const LIVE_EQUITIES_STALE_MS = 15_000; @@ -284,6 +291,12 @@ type PinnedEntry = { updatedAt: number; }; +type OptionContractDisplay = { + ticker: string; + strike: string; + expiration: string; +}; + type RetentionMetricKey = | "hotWindowEvictions" | "pinnedFetchMisses" @@ -378,7 +391,8 @@ type PausableTapeData = { export const reducePausableTapeData = ( current: PausableTapeData, incoming: T[], - paused: boolean + paused: boolean, + retentionLimit = LIVE_HOT_WINDOW ): PausableTapeData => { if (incoming.length === 0) { return current; @@ -403,7 +417,7 @@ export const reducePausableTapeData = ( if (paused) { return { visible: current.visible, - queued: mergeNewest(unseen, current.queued, LIVE_HOT_WINDOW, (evicted) => + queued: mergeNewest(unseen, current.queued, retentionLimit, (evicted) => incrementRetentionMetric("hotWindowEvictions", evicted) ), seenKeys: nextSeenKeys, @@ -413,7 +427,7 @@ export const reducePausableTapeData = ( const nextBatch = current.queued.length > 0 ? [...current.queued, ...unseen] : unseen; return { - visible: mergeNewest(nextBatch, current.visible, LIVE_HOT_WINDOW, (evicted) => + visible: mergeNewest(nextBatch, current.visible, retentionLimit, (evicted) => incrementRetentionMetric("hotWindowEvictions", evicted) ), queued: [], @@ -423,14 +437,15 @@ export const reducePausableTapeData = ( }; export const flushPausableTapeData = ( - current: PausableTapeData + current: PausableTapeData, + retentionLimit = LIVE_HOT_WINDOW ): PausableTapeData => { if (current.queued.length === 0) { return current.dropped === 0 ? current : { ...current, dropped: 0 }; } return { - visible: mergeNewest(current.queued, current.visible, LIVE_HOT_WINDOW, (evicted) => + visible: mergeNewest(current.queued, current.visible, retentionLimit, (evicted) => incrementRetentionMetric("hotWindowEvictions", evicted) ), 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 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 parsed = formatOptionContractLabel(value); + if (parsed) { + return `${parsed.ticker} ${parsed.strike} ${parsed.expiration}`; + } const normalized = normalizeContractId(value); if (!normalized) { return "Unknown contract"; @@ -1180,6 +1260,7 @@ type TapeConfig = { replaySourceKey?: string | null; onReplaySourceKey?: (key: string | null) => void; queryParams?: Record; + hotWindowLimit?: number; }; const useTape = ( @@ -1193,6 +1274,7 @@ const useTape = ( const replaySourceKey = config.replaySourceKey ?? null; const onReplaySourceKey = config.onReplaySourceKey; const queryParams = config.queryParams; + const hotWindowLimit = config.hotWindowLimit ?? LIVE_HOT_WINDOW; const [status, setStatus] = useState("connecting"); const [items, setItems] = useState([]); const [lastUpdate, setLastUpdate] = useState(null); @@ -1249,13 +1331,13 @@ const useTape = ( } setItems((prev) => - mergeNewest(buffered, prev, LIVE_HOT_WINDOW, (evicted) => + mergeNewest(buffered, prev, hotWindowLimit, (evicted) => incrementRetentionMetric("hotWindowEvictions", evicted) ) ); setLastUpdate(Date.now()); }); - }, [captureScroll, onNewItems]); + }, [captureScroll, hotWindowLimit, onNewItems]); const togglePause = useCallback(() => { setPaused((prev) => { @@ -1605,6 +1687,7 @@ type PausableTapeViewConfig = { onNewItems?: (count: number) => void; captureScroll?: () => void; getItemTs?: (item: T) => number; + retentionLimit?: number; }; const usePausableTapeView = ( @@ -1632,7 +1715,12 @@ const usePausableTapeView = ( } setData((current) => { - const next = reducePausableTapeData(current, config.sourceItems, paused); + const next = reducePausableTapeData( + current, + config.sourceItems, + paused, + config.retentionLimit ?? LIVE_HOT_WINDOW + ); if (next === current) { return current; } @@ -1645,7 +1733,14 @@ const usePausableTapeView = ( return next; }); - }, [config.enabled, config.sourceItems, config.onNewItems, config.captureScroll, paused]); + }, [ + config.enabled, + config.sourceItems, + config.onNewItems, + config.captureScroll, + config.retentionLimit, + paused + ]); useEffect(() => { if (!config.enabled || paused) { @@ -1653,7 +1748,7 @@ const usePausableTapeView = ( } setData((current) => { - const next = flushPausableTapeData(current); + const next = flushPausableTapeData(current, config.retentionLimit ?? LIVE_HOT_WINDOW); if (next === current) { return current; } @@ -1665,7 +1760,7 @@ const usePausableTapeView = ( return next; }); - }, [config.enabled, config.onNewItems, config.captureScroll, paused]); + }, [config.captureScroll, config.enabled, config.onNewItems, config.retentionLimit, paused]); const togglePause = useCallback(() => { setPaused((current) => !current); @@ -2094,7 +2189,8 @@ const useLiveSession = ( const mergeItems = ( setter: React.Dispatch>, - nextItems: T[] + nextItems: T[], + retentionLimit = LIVE_HOT_WINDOW ) => { setter((prev) => message.op === "snapshot" @@ -2106,7 +2202,7 @@ const useLiveSession = ( ) ? prev : (nextItems as T[]) - : mergeNewest(nextItems as T[], prev, LIVE_HOT_WINDOW, (evicted) => + : mergeNewest(nextItems as T[], prev, retentionLimit, (evicted) => incrementRetentionMetric("hotWindowEvictions", evicted) ) ); @@ -2114,7 +2210,7 @@ const useLiveSession = ( switch (subscription.channel) { case "options": - mergeItems(setOptions, items as OptionPrint[]); + mergeItems(setOptions, items as OptionPrint[], LIVE_HOT_WINDOW_OPTIONS); break; case "nbbo": mergeItems(setNbbo, items as OptionNBBO[]); @@ -3532,6 +3628,7 @@ const useTerminalState = () => { replayPath: "/replay/options", latestPath: "/prints/options", expectedType: "option-print", + hotWindowLimit: LIVE_HOT_WINDOW_OPTIONS, batchSize: mode === "replay" ? 120 : undefined, pollMs: mode === "replay" ? 200 : undefined, captureScroll: optionsAnchor.capture, @@ -3639,6 +3736,7 @@ const useTerminalState = () => { sourceItems: liveSession.options, lastUpdate: liveSession.lastUpdate, freshnessMs: LIVE_OPTIONS_STALE_MS, + retentionLimit: LIVE_HOT_WINDOW_OPTIONS, captureScroll: optionsAnchor.capture, onNewItems: optionsScroll.onNewItems }); @@ -4886,6 +4984,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { ) : null} {virtual.visibleItems.map((print) => { const contractId = normalizeContractId(print.option_contract_id); + const contractDisplay = formatOptionContractLabel(contractId); const quote = state.nbboMap.get(contractId); const nbboAge = quote ? Math.abs(print.ts - quote.ts) : null; const nbboStale = nbboAge !== null && nbboAge > NBBO_MAX_AGE_MS_SAFE; @@ -4896,13 +4995,36 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { return (
-
{formatContractLabel(contractId)}
+
+ {contractDisplay ? ( + <> + {contractDisplay.ticker} + {contractDisplay.strike} + {contractDisplay.expiration} + + ) : ( + formatContractLabel(contractId) + )} +
${formatPrice(print.price)} {formatSize(print.size)}x {print.exchange} - Notional ${formatUsd(notional)} - {print.conditions?.length ? {print.conditions.join(", ")} : null} + Notional ${formatCompactUsd(notional)} + {print.conditions?.map((condition) => { + const normalized = condition.toUpperCase(); + const tone = + normalized === "SWEEP" + ? "condition-sweep" + : normalized === "ISO" + ? "condition-iso" + : "condition-neutral"; + return ( + + {normalized} + + ); + })}
{quote ? (