From de6d25f046cbea0c2a2f4e49c079bd2a4aedb9c0 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Tue, 5 May 2026 01:40:10 -0400 Subject: [PATCH] Migrate terminal to smart-money profiles --- .beads/issues.jsonl | 2 +- SMART_MONEY_REBUILD_PLAN.md | 2 +- apps/web/app/terminal.test.ts | 11 + apps/web/app/terminal.tsx | 512 +++++++++++++++++++++++++++++----- 4 files changed, 452 insertions(+), 75 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7dfca78..c21246b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -7,6 +7,6 @@ {"_type":"issue","id":"islandflow-b6d","title":"Finish smart-money event-calendar enrichment","description":"Finish the smart-money event-calendar provider layer in services/refdata and connect days-to-event / expiry-after-event enrichment into compute using timestamp-available data only.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:26Z","created_by":"dirtydishes","updated_at":"2026-05-04T23:21:09Z","started_at":"2026-05-04T23:18:29Z","closed_at":"2026-05-04T23:21:09Z","close_reason":"Completed event-calendar provider and compute enrichment","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-e60","title":"Add smart-money replay evaluation harness","description":"Add replay-style live-vs-batch consistency tests plus evaluation utilities for parent-event precision/recall, calibration, abstention rate, and economic sanity checks.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:25Z","created_by":"dirtydishes","updated_at":"2026-05-04T21:35:25Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-020","title":"Rebuild synthetic smart-money scenarios","description":"Rework services/ingest-options synthetic generation around labeled parent-event templates for the six core smart-money profiles plus neutral background noise, with deterministic test/demo modes and hidden labels for tests.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:24Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:29:27Z","started_at":"2026-05-05T05:25:39Z","closed_at":"2026-05-05T05:29:27Z","close_reason":"Completed Phase 5 synthetic smart-money scenario rebuild","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-04T21:35:23Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/SMART_MONEY_REBUILD_PLAN.md b/SMART_MONEY_REBUILD_PLAN.md index 7016fbd..ae250d8 100644 --- a/SMART_MONEY_REBUILD_PLAN.md +++ b/SMART_MONEY_REBUILD_PLAN.md @@ -46,7 +46,7 @@ Acceptance: scenario tests assert intended profile wins and wrong nearby profile - [x] Emit `SmartMoneyEvent` first in compute. - [x] Derive compatibility `ClassifierHitEvent` and `AlertEvent`. - [x] Add REST/history/replay/ws/live support for smart-money events. -- [ ] Migrate terminal UI to profile-aware display. +- [x] Migrate terminal UI to profile-aware display. Acceptance: old classifier and alert endpoints still work while `/flow/smart-money`, `/history/smart-money`, `/replay/smart-money`, and `/ws/smart-money` expose the new model. diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 0c65741..48703d8 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -19,6 +19,8 @@ import { shouldRetainLiveSnapshotHistory, shouldShowEquitiesSilentFeedWarning, selectPrimaryClassifierHit, + smartMoneyProfileLabel, + smartMoneyToneForProfile, statusLabel, toggleFilterValue } from "./terminal"; @@ -318,6 +320,15 @@ describe("classifier row decoration helpers", () => { }); }); +describe("smart-money profile helpers", () => { + it("labels and colors primary profiles", () => { + expect(smartMoneyProfileLabel("institutional_directional")).toBe("Institutional Directional"); + expect(smartMoneyProfileLabel(null)).toBe("Abstained"); + expect(smartMoneyToneForProfile("event_driven")).toBe("blue"); + expect(smartMoneyToneForProfile(null)).toBe("neutral"); + }); +}); + 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 24a0e5d..87b5776 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -34,7 +34,9 @@ import type { OptionSecurityType, OptionType, OptionNBBO, - OptionPrint + OptionPrint, + SmartMoneyEvent, + SmartMoneyProfileId } from "@islandflow/types"; import { getSubscriptionKey as getLiveSubscriptionKey, @@ -239,6 +241,7 @@ type MessageType = | "equity-candle" | "equity-join" | "flow-packet" + | "smart-money" | "inferred-dark" | "classifier-hit" | "alert"; @@ -1006,6 +1009,7 @@ const LIVE_SNAPSHOT_HISTORY_CHANNELS = new Set([ "nbbo", "equities", "flow", + "smart-money", "classifier-hits" ]); @@ -1052,12 +1056,22 @@ const classifyNbboSide = (price: number, quote: OptionNBBO | null | undefined): }; type ClassifierDecor = { - hit: ClassifierHitEvent; + hit?: ClassifierHitEvent; + smartMoney?: SmartMoneyEvent; family: string; tone: string; intensity: number; }; +const SMART_MONEY_PROFILE_TONES: Record = { + institutional_directional: "green", + retail_whale: "amber", + event_driven: "blue", + vol_seller: "copper", + arbitrage: "teal", + hedge_reactive: "magenta" +}; + const CLASSIFIER_FAMILY_TONES: Record = { large_bullish_call_sweep: "green", large_bearish_put_sweep: "red", @@ -1095,6 +1109,12 @@ export const selectPrimaryClassifierHit = ( export const classifierToneForFamily = (classifierId: string): string => CLASSIFIER_FAMILY_TONES[classifierId] ?? "neutral"; +export const smartMoneyToneForProfile = (profileId: SmartMoneyProfileId | null): string => + profileId ? SMART_MONEY_PROFILE_TONES[profileId] ?? "neutral" : "neutral"; + +export const smartMoneyProfileLabel = (profileId: SmartMoneyProfileId | null): string => + profileId ? humanizeClassifierId(profileId) : "Abstained"; + const buildClassifierDecor = (hit: ClassifierHitEvent): ClassifierDecor => ({ hit, family: hit.classifier_id, @@ -1102,6 +1122,18 @@ const buildClassifierDecor = (hit: ClassifierHitEvent): ClassifierDecor => ({ intensity: clamp(hit.confidence, 0.25, 1) }); +const buildSmartMoneyDecor = (event: SmartMoneyEvent): ClassifierDecor => { + const primaryScore = + event.profile_scores.find((score) => score.profile_id === event.primary_profile_id) ?? + event.profile_scores[0]; + return { + smartMoney: event, + family: event.primary_profile_id ?? primaryScore?.profile_id ?? "abstained", + tone: event.abstained ? "neutral" : smartMoneyToneForProfile(event.primary_profile_id), + intensity: clamp(primaryScore?.probability ?? 0.25, 0.25, 1) + }; +}; + export const getOptionTableSnapshot = ( print: Pick< OptionPrint, @@ -2230,6 +2262,7 @@ type LiveSessionState = { equityQuotes: EquityQuote[]; equityJoins: EquityPrintJoin[]; flow: FlowPacket[]; + smartMoney: SmartMoneyEvent[]; classifierHits: ClassifierHitEvent[]; alerts: AlertEvent[]; inferredDark: InferredDarkEvent[]; @@ -2249,6 +2282,7 @@ const LIVE_HISTORY_ENDPOINTS: Partial([]); const [equityJoins, setEquityJoins] = useState([]); const [flow, setFlow] = useState([]); + const [smartMoney, setSmartMoney] = useState([]); const [classifierHits, setClassifierHits] = useState([]); const [alerts, setAlerts] = useState([]); const [inferredDark, setInferredDark] = useState([]); @@ -2389,6 +2426,7 @@ const useLiveSession = ( setEquityQuotes([]); setEquityJoins([]); setFlow([]); + setSmartMoney([]); setClassifierHits([]); setAlerts([]); setInferredDark([]); @@ -2489,6 +2527,9 @@ const useLiveSession = ( case "flow": mergeItems(setFlow, items as FlowPacket[]); break; + case "smart-money": + mergeItems(setSmartMoney, items as SmartMoneyEvent[]); + break; case "classifier-hits": mergeItems(setClassifierHits, items as ClassifierHitEvent[]); break; @@ -2757,6 +2798,9 @@ const useLiveSession = ( case "flow": mergeOlder(setFlow, LIVE_HOT_WINDOW); break; + case "smart-money": + mergeOlder(setSmartMoney, LIVE_HOT_WINDOW); + break; case "classifier-hits": mergeOlder(setClassifierHits, LIVE_HOT_WINDOW); break; @@ -2801,6 +2845,7 @@ const useLiveSession = ( equityQuotes, equityJoins, flow, + smartMoney, classifierHits, alerts, inferredDark, @@ -2879,14 +2924,14 @@ type CandleChartProps = { replayTime?: number | null; liveCandles?: EquityCandle[]; liveOverlayPrints?: EquityPrint[]; - classifierHits: ClassifierHitEvent[]; + smartMoneyEvents: SmartMoneyEvent[]; inferredDark: InferredDarkEvent[]; - onClassifierHitClick: (hit: ClassifierHitEvent) => void; + onSmartMoneyClick: (event: SmartMoneyEvent) => void; onInferredDarkClick: (event: InferredDarkEvent) => void; }; type MarkerAction = - | { kind: "hit"; hit: ClassifierHitEvent } + | { kind: "smart-money"; event: SmartMoneyEvent } | { kind: "dark"; event: InferredDarkEvent }; const CandleChart = ({ @@ -2896,9 +2941,9 @@ const CandleChart = ({ replayTime = null, liveCandles = [], liveOverlayPrints = [], - classifierHits, + smartMoneyEvents, inferredDark, - onClassifierHitClick, + onSmartMoneyClick, onInferredDarkClick }: CandleChartProps) => { const containerRef = useRef(null); @@ -2912,7 +2957,7 @@ const CandleChart = ({ const markerLookupRef = useRef>(new Map()); const [visibleRangeMs, setVisibleRangeMs] = useState<{ from: number; to: number } | null>(null); - const onHitClickRef = useRef(onClassifierHitClick); + const onSmartMoneyClickRef = useRef(onSmartMoneyClick); const onDarkClickRef = useRef(onInferredDarkClick); const overlayCanvasRef = useRef(null); @@ -2990,8 +3035,8 @@ const CandleChart = ({ }, [drawOverlay, ticker, intervalMs, mode]); useEffect(() => { - onHitClickRef.current = onClassifierHitClick; - }, [onClassifierHitClick]); + onSmartMoneyClickRef.current = onSmartMoneyClick; + }, [onSmartMoneyClick]); useEffect(() => { onDarkClickRef.current = onInferredDarkClick; @@ -3006,8 +3051,8 @@ const CandleChart = ({ } const { from, to } = visibleRangeMs; - const inRangeHits = classifierHits - .filter((hit) => hit.source_ts >= from && hit.source_ts <= to) + const inRangeSmartMoney = smartMoneyEvents + .filter((event) => event.source_ts >= from && event.source_ts <= to) .sort((a, b) => { const delta = a.source_ts - b.source_ts; if (delta !== 0) { @@ -3025,27 +3070,27 @@ const CandleChart = ({ return a.seq - b.seq; }); - const MAX_HIT_MARKERS = 220; + const MAX_SMART_MONEY_MARKERS = 220; const MAX_DARK_MARKERS = 120; const MAX_TOTAL_MARKERS = 320; - const cappedHits = - inRangeHits.length > MAX_HIT_MARKERS - ? inRangeHits.slice(inRangeHits.length - MAX_HIT_MARKERS) - : inRangeHits; + const cappedSmartMoney = + inRangeSmartMoney.length > MAX_SMART_MONEY_MARKERS + ? inRangeSmartMoney.slice(inRangeSmartMoney.length - MAX_SMART_MONEY_MARKERS) + : inRangeSmartMoney; const cappedDark = inRangeDark.length > MAX_DARK_MARKERS ? inRangeDark.slice(inRangeDark.length - MAX_DARK_MARKERS) : inRangeDark; - for (const hit of cappedHits) { - const direction = normalizeDirection(hit.direction); - const markerId = `hit:${hit.trace_id}:${hit.seq}`; - lookup.set(markerId, { kind: "hit", hit }); + for (const event of cappedSmartMoney) { + const direction = normalizeDirection(event.primary_direction); + const markerId = `smart-money:${event.trace_id}:${event.seq}`; + lookup.set(markerId, { kind: "smart-money", event }); markers.push({ id: markerId, - time: toChartTime(hit.source_ts), + time: toChartTime(event.source_ts), position: direction === "bullish" ? "belowBar" : "aboveBar", color: direction === "bullish" @@ -3059,7 +3104,11 @@ const CandleChart = ({ : direction === "bearish" ? "arrowDown" : "circle", - text: hit.classifier_id ? hit.classifier_id.slice(0, 3).toUpperCase() : "H" + text: event.abstained + ? "ABS" + : event.primary_profile_id + ? event.primary_profile_id.slice(0, 3).toUpperCase() + : "SM" }); } @@ -3105,7 +3154,7 @@ const CandleChart = ({ } return { markers: cappedMarkers, lookup }; - }, [classifierHits, inferredDark, visibleRangeMs]); + }, [smartMoneyEvents, inferredDark, visibleRangeMs]); useEffect(() => { if (!seriesRef.current) { @@ -3221,8 +3270,8 @@ const CandleChart = ({ if (!action) { return; } - if (action.kind === "hit") { - onHitClickRef.current(action.hit); + if (action.kind === "smart-money") { + onSmartMoneyClickRef.current(action.event); } else { onDarkClickRef.current(action.event); } @@ -3882,6 +3931,109 @@ const ClassifierHitDrawer = ({ hit, flowPacket, evidence, onClose }: ClassifierH ); }; +type SmartMoneyDrawerProps = { + event: SmartMoneyEvent; + flowPacket: FlowPacket | null; + evidence: EvidenceItem[]; + onClose: () => void; +}; + +const SmartMoneyDrawer = ({ event, flowPacket, evidence, onClose }: SmartMoneyDrawerProps) => { + const primaryScore = + event.profile_scores.find((score) => score.profile_id === event.primary_profile_id) ?? + event.profile_scores[0]; + const direction = normalizeDirection(event.primary_direction); + const evidencePrints = evidence.filter((item) => item.kind === "print"); + const unknownCount = evidence.filter((item) => item.kind === "unknown").length; + + return ( + + ); +}; + type DarkDrawerProps = { event: InferredDarkEvent; evidence: DarkEvidenceItem[]; @@ -4009,6 +4161,7 @@ const useTerminalState = () => { const [selectedAlert, setSelectedAlert] = useState(null); const [selectedDarkEvent, setSelectedDarkEvent] = useState(null); const [selectedClassifierHit, setSelectedClassifierHit] = useState(null); + const [selectedSmartMoneyEvent, setSelectedSmartMoneyEvent] = useState(null); const [selectedInstrument, setSelectedInstrument] = useState(null); const [filterInput, setFilterInput] = useState(""); const [flowFilters, setFlowFilters] = useState(() => buildDefaultFlowFilters()); @@ -4078,13 +4231,14 @@ const useTerminalState = () => { }, [mode]); useEffect(() => { - if (!selectedAlert && !selectedClassifierHit && !selectedDarkEvent) { + if (!selectedAlert && !selectedClassifierHit && !selectedDarkEvent && !selectedSmartMoneyEvent) { return; } const dismissDrawers = () => { setSelectedAlert(null); setSelectedClassifierHit(null); + setSelectedSmartMoneyEvent(null); setSelectedDarkEvent(null); }; @@ -4108,7 +4262,7 @@ const useTerminalState = () => { document.removeEventListener("mousedown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; - }, [selectedAlert, selectedClassifierHit, selectedDarkEvent]); + }, [selectedAlert, selectedClassifierHit, selectedDarkEvent, selectedSmartMoneyEvent]); const optionsScroll = useListScroll(); const equitiesScroll = useListScroll(); @@ -4250,6 +4404,19 @@ const useTerminalState = () => { onNewItems: classifierScroll.onNewItems, getReplayKey: disableReplayGrouping }); + const smartMoney = useTape({ + mode, + liveEnabled: false, + wsPath: "/ws/smart-money", + replayPath: "/replay/smart-money", + latestPath: "/flow/smart-money", + expectedType: "smart-money", + batchSize: mode === "replay" ? 120 : undefined, + pollMs: mode === "replay" ? 200 : undefined, + captureScroll: classifierAnchor.capture, + onNewItems: classifierScroll.onNewItems, + getReplayKey: disableReplayGrouping + }); const liveOptions = usePausableTapeView({ enabled: mode === "live", @@ -4302,6 +4469,10 @@ const useTerminalState = () => { mode === "live" ? toStaticTapeState(liveSession.status, liveSession.classifierHits, liveSession.lastUpdate) : classifierHits; + const smartMoneyFeed = + mode === "live" + ? toStaticTapeState(liveSession.status, liveSession.smartMoney, liveSession.lastUpdate) + : smartMoney; const inferredDarkFeed = mode === "live" ? toStaticTapeState(liveSession.status, liveSession.inferredDark, liveSession.lastUpdate) @@ -4329,7 +4500,7 @@ const useTerminalState = () => { useLayoutEffect(() => { classifierAnchor.apply(); - }, [classifierHitsFeed.items, classifierAnchor.apply]); + }, [smartMoneyFeed.items, classifierHitsFeed.items, classifierAnchor.apply]); const nbboMap = useMemo(() => { const map = new Map(); @@ -4595,6 +4766,7 @@ const useTerminalState = () => { } setSelectedDarkEvent(null); setSelectedClassifierHit(null); + setSelectedSmartMoneyEvent(null); }, [mode]); const extractPacketContract = useCallback((packet: FlowPacket): string => { @@ -4634,6 +4806,19 @@ const useTerminalState = () => { return map; }, [classifierHitsFeed.items, extractPacketIdFromClassifierHitTrace]); + const smartMoneyByPacketId = useMemo(() => { + const map = new Map(); + for (const event of smartMoneyFeed.items) { + for (const packetId of event.packet_ids) { + const existing = map.get(packetId); + if (!existing || event.source_ts > existing.source_ts || event.seq > existing.seq) { + map.set(packetId, event); + } + } + } + return map; + }, [smartMoneyFeed.items]); + const packetIdByOptionTraceId = useMemo(() => { const map = new Map(); for (const packet of flowFeed.items) { @@ -4647,13 +4832,18 @@ const useTerminalState = () => { const classifierDecorByOptionTraceId = useMemo(() => { const map = new Map(); for (const [traceId, packetId] of packetIdByOptionTraceId) { + const smartMoneyEvent = smartMoneyByPacketId.get(packetId); + if (smartMoneyEvent) { + map.set(traceId, buildSmartMoneyDecor(smartMoneyEvent)); + continue; + } const primary = selectPrimaryClassifierHit(classifierHitsByPacketId.get(packetId) ?? []); if (primary) { map.set(traceId, buildClassifierDecor(primary)); } } return map; - }, [classifierHitsByPacketId, packetIdByOptionTraceId]); + }, [classifierHitsByPacketId, packetIdByOptionTraceId, smartMoneyByPacketId]); const selectedClassifierPacketId = useMemo(() => { if (!selectedClassifierHit) { @@ -4721,6 +4911,90 @@ const useTerminalState = () => { }); }, [resolvedFlowPacketMap, resolvedOptionPrintMap, selectedClassifierHit, selectedClassifierPacketId]); + const selectedSmartMoneyFlowPacket = useMemo(() => { + const packetId = selectedSmartMoneyEvent?.packet_ids[0]; + return packetId ? resolvedFlowPacketMap.get(packetId) ?? null : null; + }, [resolvedFlowPacketMap, selectedSmartMoneyEvent]); + + const selectedSmartMoneyEvidence = useMemo((): EvidenceItem[] => { + if (!selectedSmartMoneyEvent) { + return []; + } + return selectedSmartMoneyEvent.member_print_ids.map((id) => { + const print = resolvedOptionPrintMap.get(id); + if (print) { + return { kind: "print", id, print }; + } + return { kind: "unknown", id }; + }); + }, [resolvedOptionPrintMap, selectedSmartMoneyEvent]); + + useEffect(() => { + if (!selectedSmartMoneyEvent || mode !== "live") { + return; + } + + const missingPacketIds = selectedSmartMoneyEvent.packet_ids.filter((id) => !resolvedFlowPacketMap.has(id)); + if (missingPacketIds.length > 0) { + incrementRetentionMetric("pinnedFetchMisses", missingPacketIds.length); + void Promise.all( + missingPacketIds.map(async (packetId) => { + const response = await fetch(buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`)); + if (!response.ok) { + throw new Error(await readErrorDetail(response)); + } + const payload = (await response.json()) as { data?: FlowPacket | null }; + return payload.data ?? null; + }) + ) + .then((packets) => { + const next = new Map(); + for (const packet of packets) { + if (packet) { + next.set(packet.id, packet); + } + } + if (next.size > 0) { + setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, next, Date.now())); + } + }) + .catch((error) => { + incrementRetentionMetric("pinnedFetchFailures", 1); + console.warn("Failed to fetch smart-money flow packets", error); + }); + } + + const missingPrintIds = selectedSmartMoneyEvent.member_print_ids.filter((id) => !resolvedOptionPrintMap.has(id)); + if (missingPrintIds.length === 0) { + return; + } + incrementRetentionMetric("pinnedFetchMisses", missingPrintIds.length); + const url = new URL(buildApiUrl("/option-prints/by-trace")); + for (const traceId of missingPrintIds) { + url.searchParams.append("trace_id", traceId); + } + void fetch(url.toString()) + .then(async (response) => { + if (!response.ok) { + throw new Error(await readErrorDetail(response)); + } + return response.json(); + }) + .then((payload: { data?: OptionPrint[] }) => { + const next = new Map(); + for (const item of payload.data ?? []) { + next.set(item.trace_id, item); + } + if (next.size > 0) { + setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, Date.now())); + } + }) + .catch((error) => { + incrementRetentionMetric("pinnedFetchFailures", 1); + console.warn("Failed to fetch smart-money option prints", error); + }); + }, [mode, resolvedFlowPacketMap, resolvedOptionPrintMap, selectedSmartMoneyEvent]); + const inferAlertUnderlying = useCallback( (alert: AlertEvent): string | null => { const fromTrace = extractUnderlyingFromTrace(alert.trace_id); @@ -4932,6 +5206,9 @@ const useTerminalState = () => { if (selectedClassifierPacketId) { keys.add(selectedClassifierPacketId); } + for (const packetId of selectedSmartMoneyEvent?.packet_ids ?? []) { + keys.add(packetId); + } for (const alert of visibleAlerts) { const packetId = alert.evidence_refs[0]; if (packetId) { @@ -4939,7 +5216,7 @@ const useTerminalState = () => { } } return keys; - }, [selectedAlert, selectedClassifierPacketId, visibleAlerts]); + }, [selectedAlert, selectedClassifierPacketId, selectedSmartMoneyEvent, visibleAlerts]); const activePinnedOptionKeys = useMemo(() => { const keys = new Set(); @@ -4953,11 +5230,14 @@ const useTerminalState = () => { keys.add(id); } } + for (const id of selectedSmartMoneyEvent?.member_print_ids ?? []) { + keys.add(id); + } for (const id of visibleAlertEvidenceRefs) { keys.add(id); } return keys; - }, [selectedAlert, selectedClassifierFlowPacket, visibleAlertEvidenceRefs]); + }, [selectedAlert, selectedClassifierFlowPacket, selectedSmartMoneyEvent, visibleAlertEvidenceRefs]); const activePinnedJoinKeys = useMemo(() => { const keys = new Set(); @@ -5009,10 +5289,17 @@ const useTerminalState = () => { }); }, [classifierHitsFeed.items, extractUnderlyingFromTrace, matchesTicker, tickerSet]); - const chartClassifierHits = useMemo(() => { + const filteredSmartMoneyEvents = useMemo(() => { + if (tickerSet.size === 0) { + return smartMoneyFeed.items; + } + return smartMoneyFeed.items.filter((event) => matchesTicker(event.underlying_id)); + }, [matchesTicker, smartMoneyFeed.items, tickerSet]); + + const chartSmartMoneyEvents = useMemo(() => { const desired = chartTicker.toUpperCase(); - return classifierHitsFeed.items - .filter((hit) => extractUnderlyingFromTrace(hit.trace_id) === desired) + return smartMoneyFeed.items + .filter((event) => event.underlying_id.toUpperCase() === desired) .sort((a, b) => { const delta = a.source_ts - b.source_ts; if (delta !== 0) { @@ -5020,7 +5307,7 @@ const useTerminalState = () => { } return a.seq - b.seq; }); - }, [chartTicker, classifierHitsFeed.items, extractUnderlyingFromTrace]); + }, [chartTicker, smartMoneyFeed.items]); const chartInferredDark = useMemo(() => { const desired = chartTicker.toUpperCase(); @@ -5058,27 +5345,37 @@ const useTerminalState = () => { if (alert) { setSelectedClassifierHit(null); setSelectedDarkEvent(null); + setSelectedSmartMoneyEvent(null); setSelectedAlert(alert); return; } setSelectedAlert(null); setSelectedDarkEvent(null); + setSelectedSmartMoneyEvent(null); setSelectedClassifierHit(hit); }, [findAlertForClassifierHit] ); - const handleClassifierMarkerClick = useCallback( - (hit: ClassifierHitEvent) => { - openFromClassifierHit(hit); + const openFromSmartMoneyEvent = useCallback((event: SmartMoneyEvent) => { + setSelectedAlert(null); + setSelectedClassifierHit(null); + setSelectedDarkEvent(null); + setSelectedSmartMoneyEvent(event); + }, []); + + const handleSmartMoneyMarkerClick = useCallback( + (event: SmartMoneyEvent) => { + openFromSmartMoneyEvent(event); }, - [openFromClassifierHit] + [openFromSmartMoneyEvent] ); const handleDarkMarkerClick = useCallback((event: InferredDarkEvent) => { setSelectedAlert(null); setSelectedClassifierHit(null); + setSelectedSmartMoneyEvent(null); setSelectedDarkEvent(event); }, []); @@ -5089,6 +5386,7 @@ const useTerminalState = () => { inferredDarkFeed.lastUpdate, flowFeed.lastUpdate, alertsFeed.lastUpdate, + smartMoneyFeed.lastUpdate, classifierHitsFeed.lastUpdate ] .filter((value): value is number => value !== null) @@ -5099,6 +5397,7 @@ const useTerminalState = () => { inferredDarkFeed.lastUpdate, flowFeed.lastUpdate, alertsFeed.lastUpdate, + smartMoneyFeed.lastUpdate, classifierHitsFeed.lastUpdate ]); @@ -5113,6 +5412,8 @@ const useTerminalState = () => { setSelectedDarkEvent, selectedClassifierHit, setSelectedClassifierHit, + selectedSmartMoneyEvent, + setSelectedSmartMoneyEvent, selectedInstrument, setSelectedInstrument, selectedInstrumentLabel, @@ -5135,6 +5436,7 @@ const useTerminalState = () => { inferredDark: inferredDarkFeed, flow: flowFeed, alerts: alertsFeed, + smartMoney: smartMoneyFeed, classifierHits: classifierHitsFeed, liveSession, activeTickers, @@ -5155,17 +5457,21 @@ const useTerminalState = () => { selectedClassifierPacketId, selectedClassifierFlowPacket, selectedClassifierEvidence, + selectedSmartMoneyFlowPacket, + selectedSmartMoneyEvidence, filteredOptions, filteredEquities, equitiesSilentWarning, filteredInferredDark, filteredFlow, filteredAlerts, + filteredSmartMoneyEvents, filteredClassifierHits, - chartClassifierHits, + chartSmartMoneyEvents, chartInferredDark, + openFromSmartMoneyEvent, openFromClassifierHit, - handleClassifierMarkerClick, + handleSmartMoneyMarkerClick, handleDarkMarkerClick, lastSeen, toggleMode: () => { @@ -5618,11 +5924,21 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { type="button" {...commonProps} key={`${print.trace_id}-${print.seq}`} - onClick={() => state.openFromClassifierHit(decor.hit)} + onClick={() => + decor.smartMoney + ? state.openFromSmartMoneyEvent(decor.smartMoney) + : decor.hit + ? state.openFromClassifierHit(decor.hit) + : undefined + } onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); - state.openFromClassifierHit(decor.hit); + if (decor.smartMoney) { + state.openFromSmartMoneyEvent(decor.smartMoney); + } else if (decor.hit) { + state.openFromClassifierHit(decor.hit); + } } }} > @@ -5951,6 +6267,7 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) => onClick={() => { state.setSelectedDarkEvent(null); state.setSelectedClassifierHit(null); + state.setSelectedSmartMoneyEvent(null); state.setSelectedAlert(alert); }} > @@ -5982,8 +6299,22 @@ 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, 44); + const smartMoneyItems = limit ? state.filteredSmartMoneyEvents.slice(0, limit) : state.filteredSmartMoneyEvents; + const legacyItems = + smartMoneyItems.length === 0 + ? limit + ? state.filteredClassifierHits.slice(0, limit) + : state.filteredClassifierHits + : []; + const items: Array = + smartMoneyItems.length > 0 ? smartMoneyItems : legacyItems; + const virtual = useVirtualList( + items, + state.classifierScroll.listRef, + !limit, + 44 + ); + const showingSmartMoney = smartMoneyItems.length > 0; return ( { title="Rules" status={ } actions={ { {state.tickerSet.size > 0 ? "No classifier hits match the current filter." : state.mode === "live" - ? "No classifier hits yet. Start compute." + ? "No smart-money profiles yet. Start compute." : "Replay queue empty. Ensure ClickHouse has data."} ) : (
-
+
TIME - RULE + PROFILE DIR - CONF + PROB NOTE
{virtual.topSpacerHeight > 0 ? (
) : null} - {virtual.visibleItems.map((hit) => { - const direction = normalizeDirection(hit.direction); + {showingSmartMoney ? (virtual.visibleItems as SmartMoneyEvent[]).map((event) => { + const primaryScore = + event.profile_scores.find((score) => score.profile_id === event.primary_profile_id) ?? + event.profile_scores[0]; + const direction = normalizeDirection(event.primary_direction); return ( ); + }) : (virtual.visibleItems as ClassifierHitEvent[]).map((hit) => { + const direction = normalizeDirection(hit.direction); + return ( + + ); })} {virtual.bottomSpacerHeight > 0 ? (
@@ -6130,6 +6486,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => { onClick={() => { state.setSelectedAlert(null); state.setSelectedClassifierHit(null); + state.setSelectedSmartMoneyEvent(null); state.setSelectedDarkEvent(event); }} > @@ -6188,9 +6545,9 @@ const ChartPane = ({ title = "Chart" }: ChartPaneProps) => { replayTime={state.equities.replayTime} liveCandles={state.liveSession.chartCandles} liveOverlayPrints={state.liveSession.chartOverlay} - classifierHits={state.chartClassifierHits} + smartMoneyEvents={state.chartSmartMoneyEvents} inferredDark={state.chartInferredDark} - onClassifierHitClick={state.handleClassifierMarkerClick} + onSmartMoneyClick={state.handleSmartMoneyMarkerClick} onInferredDarkClick={state.handleDarkMarkerClick} /> @@ -6199,7 +6556,7 @@ const ChartPane = ({ title = "Chart" }: ChartPaneProps) => { const FocusPane = () => { const state = useTerminal(); - const hits = state.chartClassifierHits.slice(-10).reverse(); + const hits = state.chartSmartMoneyEvents.slice(-10).reverse(); const dark = state.chartInferredDark.slice(-10).reverse(); return ( @@ -6220,13 +6577,13 @@ const FocusPane = () => { className="row row-button" key={`${hit.trace_id}-${hit.seq}`} type="button" - onClick={() => state.openFromClassifierHit(hit)} + onClick={() => state.openFromSmartMoneyEvent(hit)} >
-
{humanizeClassifierId(hit.classifier_id)}
+
{smartMoneyProfileLabel(hit.primary_profile_id)}
- - {normalizeDirection(hit.direction)} + + {normalizeDirection(hit.primary_direction)} {formatTime(hit.source_ts)}
@@ -6396,6 +6753,15 @@ export function TerminalAppShell({ children }: { children: ReactNode }) { /> ) : null} + {state.selectedSmartMoneyEvent ? ( + state.setSelectedSmartMoneyEvent(null)} + /> + ) : null} + {state.selectedDarkEvent ? (