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:
parent
da942079f3
commit
9131e046cb
5 changed files with 235 additions and 18 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue