diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 64fe95c..9ea6697 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -897,6 +897,7 @@ h3 { background: linear-gradient(180deg, rgba(245, 166, 35, 0.07), rgba(255, 255, 255, 0.018)); } +.data-table-shell, .options-table-wrap { display: flex; flex: 1 1 auto; @@ -905,6 +906,191 @@ h3 { overflow: hidden; } +.data-table-wrap { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + background: rgba(5, 8, 12, 0.42); +} + +.data-table { + display: block; + min-width: 980px; +} + +.data-table-options { + min-width: 1280px; +} + +.data-table-equities { + min-width: 660px; +} + +.data-table-flow { + min-width: 1260px; +} + +.data-table-alerts { + min-width: 900px; +} + +.data-table-classifier { + min-width: 760px; +} + +.data-table-dark { + min-width: 820px; +} + +.data-table-head, +.data-table-row { + display: grid; + align-items: center; + column-gap: 8px; +} + +.data-table-head { + position: sticky; + top: 0; + z-index: 2; + min-height: 30px; + padding: 0 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.095); + background: rgba(8, 11, 16, 0.98); + color: var(--text-faint); + font-size: 0.64rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.data-table-row { + width: 100%; + min-height: 40px; + padding: 0 10px; + border: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.055); + background: rgba(255, 255, 255, 0.008); + color: inherit; + font: inherit; + text-align: left; +} + +.data-table-row:nth-child(even) { + background: rgba(255, 255, 255, 0.022); +} + +.data-table-row:hover, +.data-table-row:focus-visible { + outline: none; + background: rgba(245, 166, 35, 0.055); +} + +.data-table-row-button { + cursor: pointer; +} + +.data-table-row-options { + min-height: 36px; +} + +.data-table-row-equities { + min-height: 34px; +} + +.data-table-row-flow, +.data-table-row-alerts, +.data-table-row-classifier, +.data-table-row-dark { + min-height: 44px; +} + +.data-table-row-classified { + background: + linear-gradient(90deg, rgba(var(--classifier-rgb, 192, 200, 210), calc(0.02 + var(--classifier-intensity, 0) * 0.12)), transparent 62%), + rgba(255, 255, 255, 0.008); +} + +.data-table-row-classified:hover, +.data-table-row-classified:focus-visible { + background: + linear-gradient(90deg, rgba(var(--classifier-rgb, 192, 200, 210), calc(0.04 + var(--classifier-intensity, 0) * 0.18)), transparent 68%), + rgba(245, 166, 35, 0.04); +} + +.data-table-row-classified.is-classified { + border-left: 3px solid rgba(var(--classifier-rgb), calc(0.35 + var(--classifier-intensity) * 0.45)); + padding-left: 7px; +} + +.data-table-row-warn, +.data-table-row-severity-high, +.data-table-row-direction-bearish { + border-left: 3px solid rgba(255, 107, 95, 0.58); + padding-left: 7px; +} + +.data-table-row-severity-medium, +.data-table-row-direction-neutral { + border-left: 3px solid rgba(77, 163, 255, 0.46); + padding-left: 7px; +} + +.data-table-row-severity-low, +.data-table-row-direction-bullish { + border-left: 3px solid rgba(37, 193, 122, 0.5); + padding-left: 7px; +} + +.data-table-options .data-table-head, +.data-table-options .data-table-row { + grid-template-columns: minmax(72px, 0.8fr) minmax(50px, 0.55fr) minmax(64px, 0.7fr) minmax(58px, 0.6fr) minmax(34px, 0.35fr) minmax(62px, 0.65fr) minmax(104px, 1fr) minmax(54px, 0.55fr) minmax(66px, 0.7fr) minmax(48px, 0.5fr) minmax(42px, 0.45fr) minmax(92px, 0.9fr); +} + +.data-table-equities .data-table-head, +.data-table-equities .data-table-row { + grid-template-columns: minmax(76px, 0.9fr) minmax(70px, 0.8fr) minmax(76px, 0.8fr) minmax(70px, 0.75fr) minmax(80px, 0.8fr) minmax(76px, 0.75fr); +} + +.data-table-flow .data-table-head, +.data-table-flow .data-table-row { + grid-template-columns: minmax(148px, 1.1fr) minmax(180px, 1.4fr) minmax(62px, 0.45fr) minmax(70px, 0.5fr) minmax(88px, 0.7fr) minmax(74px, 0.55fr) minmax(132px, 1fr) minmax(110px, 0.8fr) minmax(210px, 1.6fr); +} + +.data-table-alerts .data-table-head, +.data-table-alerts .data-table-row { + grid-template-columns: minmax(76px, 0.75fr) minmax(170px, 1.4fr) minmax(52px, 0.45fr) minmax(58px, 0.45fr) minmax(52px, 0.4fr) minmax(66px, 0.55fr) minmax(260px, 2fr); +} + +.data-table-classifier .data-table-head, +.data-table-classifier .data-table-row { + grid-template-columns: minmax(76px, 0.75fr) minmax(180px, 1.45fr) minmax(70px, 0.6fr) minmax(74px, 0.65fr) minmax(300px, 2.2fr); +} + +.data-table-dark .data-table-head, +.data-table-dark .data-table-row { + grid-template-columns: minmax(76px, 0.75fr) minmax(170px, 1.35fr) minmax(76px, 0.65fr) minmax(74px, 0.65fr) minmax(74px, 0.65fr) minmax(260px, 2fr); +} + +.data-table-cell { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.72rem; +} + +.data-table-cell-number { + font-family: var(--font-mono), monospace; + font-variant-numeric: tabular-nums; +} + +.data-table-spacer { + min-width: 100%; + pointer-events: none; +} + .options-table { display: flex; min-height: 0; diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 1092460..09b4d18 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -5343,7 +5343,7 @@ type OptionsPaneProps = { const OptionsPane = ({ limit }: OptionsPaneProps) => { const state = useTerminal(); const items = limit ? state.filteredOptions.slice(0, limit) : state.filteredOptions; - const virtual = useVirtualList(items, state.optionsScroll.listRef, !limit, 34); + const virtual = useVirtualList(items, state.optionsScroll.listRef, !limit, 36); return ( { /> } > -
+
{items.length === 0 ? (
{state.tickerSet.size > 0 @@ -5381,24 +5381,24 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { : "Replay queue empty. Ensure ClickHouse has data."}
) : ( -
-
- TIME - SYM - EXP - STRIKE - C/P - SPOT - DETAILS - TYPE - VALUE - SIDE - IV - CLASSIFIER -
-
+
+
+
+ TIME + SYM + EXP + STRIKE + C/P + SPOT + DETAILS + TYPE + VALUE + SIDE + IV + CLASSIFIER +
{virtual.topSpacerHeight > 0 ? ( -
+
) : null} {virtual.visibleItems.map((print) => { const contractId = normalizeContractId(print.option_contract_id); @@ -5415,31 +5415,31 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { const iv = print.execution_iv; const decor = state.classifierDecorByOptionTraceId.get(print.trace_id); const commonProps = { - className: `options-table-row${decor ? ` is-classified classifier-${decor.tone}` : ""}`, + 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 }; const cells = ( <> - {formatTime(print.ts)} - {contractDisplay?.ticker ?? parsed?.root ?? formatContractLabel(contractId)} - {contractDisplay?.expiration ?? parsed?.expiry ?? "--"} - {contractDisplay?.strike.replace(/[CP]$/, "") ?? "--"} - {parsed?.right ?? contractDisplay?.strike.slice(-1) ?? "--"} - {typeof spot === "number" ? formatPrice(spot) : "--"} - + {formatTime(print.ts)} + {contractDisplay?.ticker ?? parsed?.root ?? formatContractLabel(contractId)} + {contractDisplay?.expiration ?? parsed?.expiry ?? "--"} + {contractDisplay?.strike.replace(/[CP]$/, "") ?? "--"} + {parsed?.right ?? contractDisplay?.strike.slice(-1) ?? "--"} + {typeof spot === "number" ? formatPrice(spot) : "--"} + {formatSize(print.size)}@{formatPrice(print.price)}_{nbboSide ?? "--"} - {print.option_type ?? "--"} - ${formatCompactUsd(notional)} - + {print.option_type ?? "--"} + ${formatCompactUsd(notional)} + {nbboSide ? ( {nbboSide} ) : ( "--" )} - {typeof iv === "number" ? formatPct(iv) : "--"} - {decor ? humanizeClassifierId(decor.family) : "--"} + {typeof iv === "number" ? formatPct(iv) : "--"} + {decor ? humanizeClassifierId(decor.family) : "--"} ); @@ -5465,7 +5465,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { ); })} {virtual.bottomSpacerHeight > 0 ? ( -
+
) : null}
@@ -5483,7 +5483,7 @@ type EquitiesPaneProps = { const EquitiesPane = ({ limit }: EquitiesPaneProps) => { const state = useTerminal(); const items = limit ? state.filteredEquities.slice(0, limit) : state.filteredEquities; - const virtual = useVirtualList(items, state.equitiesScroll.listRef, !limit, 78); + const virtual = useVirtualList(items, state.equitiesScroll.listRef, !limit, 36); return ( { /> } > -
+
{items.length === 0 ? (
{state.tickerSet.size > 0 @@ -5523,34 +5523,36 @@ const EquitiesPane = ({ limit }: EquitiesPaneProps) => { : "Replay queue empty. Ensure ClickHouse has data."}
) : ( - <> +
+
+
+ TIME + SYM + PRICE + SIZE + VENUE + TAPE +
{virtual.topSpacerHeight > 0 ? ( -
+
) : null} {virtual.visibleItems.map((print) => ( -
-
-
{print.underlying_id}
-
- ${formatPrice(print.price)} - {formatSize(print.size)}x - {print.exchange} - {print.offExchangeFlag ? ( - Off-Ex - ) : ( - Lit - )} -
-
-
{formatTime(print.ts)}
+
+ {formatTime(print.ts)} + {print.underlying_id} + ${formatPrice(print.price)} + {formatSize(print.size)}x + {print.exchange} + {print.offExchangeFlag ? "Off-Ex" : "Lit"}
))} {virtual.bottomSpacerHeight > 0 ? ( -
+
) : null} - {!limit ? : null} - +
+
)} + {!limit ? : null}
); @@ -5564,7 +5566,7 @@ type FlowPaneProps = { const FlowPane = ({ limit, title = "Flow" }: FlowPaneProps) => { const state = useTerminal(); const items = limit ? state.filteredFlow.slice(0, limit) : state.filteredFlow; - const virtual = useVirtualList(items, state.flowScroll.listRef, !limit, 104); + const virtual = useVirtualList(items, state.flowScroll.listRef, !limit, 44); return ( { /> } > -
+
{items.length === 0 ? (
{state.tickerSet.size > 0 @@ -5602,9 +5604,21 @@ const FlowPane = ({ limit, title = "Flow" }: FlowPaneProps) => { : "Replay queue empty. Ensure ClickHouse has data."}
) : ( - <> +
+
+
+ TIME + CONTRACT + PRINTS + SIZE + NOTIONAL + WINDOW + STRUCTURE + NBBO + QUALITY +
{virtual.topSpacerHeight > 0 ? ( -
+
) : null} {virtual.visibleItems.map((packet) => { const features = packet.features ?? {}; @@ -5638,59 +5652,46 @@ const FlowPane = ({ limit, title = "Flow" }: FlowPaneProps) => { const nbboAge = parseNumber(packet.join_quality.nbbo_age_ms, Number.NaN); const nbboStale = parseNumber(packet.join_quality.nbbo_stale, 0) > 0; const nbboMissing = parseNumber(packet.join_quality.nbbo_missing, 0) > 0; + const structureLabel = structureType + ? `${structureType.replace(/_/g, " ")}${structureRights ? ` ${structureRights}` : ""}${structureLegs > 0 ? ` ${structureLegs}L` : ""}${structureStrikes > 0 ? ` ${structureStrikes}K` : ""}` + : "--"; + const nbboLabel = Number.isFinite(nbboBid) && Number.isFinite(nbboAsk) + ? `${formatPrice(nbboBid)} x ${formatPrice(nbboAsk)}` + : Number.isFinite(nbboMid) + ? `Mid ${formatPrice(nbboMid)}` + : "--"; + const qualityLabel = [ + Number.isFinite(aggressiveCoverage) && aggressiveCoverage > 0 + ? `Agg ${formatPct(aggressiveBuyRatio)}/${formatPct(aggressiveSellRatio)} ${formatPct(aggressiveCoverage)} cov` + : null, + Number.isFinite(insideRatio) && insideRatio > 0 ? `In ${formatPct(insideRatio)}` : null, + Number.isFinite(nbboSpread) ? `Spr ${formatPrice(nbboSpread)}` : null, + Number.isFinite(nbboAge) ? `${Math.round(nbboAge)}ms` : null, + nbboStale ? "Stale" : null, + nbboMissing ? "Missing" : null + ].filter(Boolean).join(" | "); return ( -
-
-
{contract}
-
- {formatFlowMetric(count)} prints - {formatFlowMetric(totalSize)} size - Notional ${formatUsd(notional)} - {windowMs > 0 ? {formatFlowMetric(windowMs, "ms")} : null} - {structureType ? ( - - {structureType.replace(/_/g, " ")} - {structureRights ? ` ${structureRights}` : ""} - {structureLegs > 0 ? ` ${structureLegs}L` : ""} - {structureStrikes > 0 ? ` ${structureStrikes}K` : ""} - - ) : null} - {Number.isFinite(aggressiveCoverage) && aggressiveCoverage > 0 ? ( - - Agg {formatPct(aggressiveBuyRatio)} / {formatPct(aggressiveSellRatio)} - {Number.isFinite(insideRatio) && insideRatio > 0 - ? ` · In ${formatPct(insideRatio)}` - : ""} - {` · ${formatPct(aggressiveCoverage)} cov`} - - ) : null} - {Number.isFinite(nbboBid) && Number.isFinite(nbboAsk) ? ( - - NBBO ${formatPrice(nbboBid)} x ${formatPrice(nbboAsk)} - - ) : null} - {Number.isFinite(nbboMid) ? Mid ${formatPrice(nbboMid)} : null} - {Number.isFinite(nbboSpread) ? ( - Spread ${formatPrice(nbboSpread)} - ) : null} - {Number.isFinite(nbboAge) ? {Math.round(nbboAge)}ms : null} - {nbboStale ? NBBO stale : null} - {nbboMissing ? NBBO missing : null} -
-
-
- {formatTime(startTs)} → {formatTime(endTs)} -
+
+ {formatTime(startTs)} → {formatTime(endTs)} + {contract} + {formatFlowMetric(count)} + {formatFlowMetric(totalSize)} + ${formatUsd(notional)} + {windowMs > 0 ? formatFlowMetric(windowMs, "ms") : "--"} + {structureLabel} + {nbboLabel} + {qualityLabel || "--"}
); })} {virtual.bottomSpacerHeight > 0 ? ( -
+
) : null} - {!limit ? : null} - +
+
)} + {!limit ? : null}
); @@ -5705,7 +5706,7 @@ type AlertsPaneProps = { const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) => { const state = useTerminal(); const items = limit ? state.filteredAlerts.slice(0, limit) : state.filteredAlerts; - const virtual = useVirtualList(items, state.alertsScroll.listRef, !limit, 92); + const virtual = useVirtualList(items, state.alertsScroll.listRef, !limit, 46); return ( } > {withStrip ? : null} -
+
{items.length === 0 ? (
{state.tickerSet.size > 0 @@ -5743,9 +5744,19 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) => : "Replay queue empty. Ensure ClickHouse has data."}
) : ( - <> +
+
+
+ TIME + ALERT + SEV + SCORE + HITS + DIR + NOTE +
{virtual.topSpacerHeight > 0 ? ( -
+
) : null} {virtual.visibleItems.map((alert) => { const primary = alert.hits[0]; @@ -5754,7 +5765,7 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) => return ( ); })} {virtual.bottomSpacerHeight > 0 ? ( -
+
) : null} - {!limit ? : null} - +
+
)} + {!limit ? : null}
); @@ -5800,7 +5804,7 @@ type ClassifierPaneProps = { const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => { const state = useTerminal(); const items = limit ? state.filteredClassifierHits.slice(0, limit) : state.filteredClassifierHits; - const virtual = useVirtualList(items, state.classifierScroll.listRef, !limit, 88); + const virtual = useVirtualList(items, state.classifierScroll.listRef, !limit, 44); return ( { /> } > -
+
{items.length === 0 ? (
{state.tickerSet.size > 0 @@ -5837,37 +5841,42 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => { : "Replay queue empty. Ensure ClickHouse has data."}
) : ( - <> +
+
+
+ TIME + RULE + DIR + CONF + NOTE +
{virtual.topSpacerHeight > 0 ? ( -
+
) : null} {virtual.visibleItems.map((hit) => { const direction = normalizeDirection(hit.direction); return ( ); })} {virtual.bottomSpacerHeight > 0 ? ( -
+
) : null} - {!limit ? : null} - +
+
)} + {!limit ? : null}
); @@ -5881,7 +5890,7 @@ type DarkPaneProps = { const DarkPane = ({ limit, className }: DarkPaneProps) => { const state = useTerminal(); const items = limit ? state.filteredInferredDark.slice(0, limit) : state.filteredInferredDark; - const virtual = useVirtualList(items, state.darkScroll.listRef, !limit, 88); + const virtual = useVirtualList(items, state.darkScroll.listRef, !limit, 44); return ( { /> } > -
+
{items.length === 0 ? (
{state.tickerSet.size > 0 @@ -5918,9 +5927,18 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => { : "Replay queue empty. Ensure ClickHouse has data."}
) : ( - <> +
+
+
+ TIME + TYPE + SYM + CONF + EVIDENCE + NOTE +
{virtual.topSpacerHeight > 0 ? ( -
+
) : null} {virtual.visibleItems.map((event) => { const underlying = inferDarkUnderlying(event, state.equityPrintMap, state.equityJoinMap); @@ -5928,7 +5946,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => { return ( ); })} {virtual.bottomSpacerHeight > 0 ? ( -
+
) : null} - {!limit ? : null} - +
+
)} + {!limit ? : null}
);