Migrate terminal to smart-money profiles

This commit is contained in:
dirtydishes 2026-05-05 01:40:10 -04:00
parent 86661df7ae
commit de6d25f046
4 changed files with 452 additions and 75 deletions

View file

@ -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-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-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-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-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} {"_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}

View file

@ -46,7 +46,7 @@ Acceptance: scenario tests assert intended profile wins and wrong nearby profile
- [x] Emit `SmartMoneyEvent` first in compute. - [x] Emit `SmartMoneyEvent` first in compute.
- [x] Derive compatibility `ClassifierHitEvent` and `AlertEvent`. - [x] Derive compatibility `ClassifierHitEvent` and `AlertEvent`.
- [x] Add REST/history/replay/ws/live support for smart-money events. - [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. 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.

View file

@ -19,6 +19,8 @@ import {
shouldRetainLiveSnapshotHistory, shouldRetainLiveSnapshotHistory,
shouldShowEquitiesSilentFeedWarning, shouldShowEquitiesSilentFeedWarning,
selectPrimaryClassifierHit, selectPrimaryClassifierHit,
smartMoneyProfileLabel,
smartMoneyToneForProfile,
statusLabel, statusLabel,
toggleFilterValue toggleFilterValue
} from "./terminal"; } 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", () => { 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

@ -34,7 +34,9 @@ import type {
OptionSecurityType, OptionSecurityType,
OptionType, OptionType,
OptionNBBO, OptionNBBO,
OptionPrint OptionPrint,
SmartMoneyEvent,
SmartMoneyProfileId
} from "@islandflow/types"; } from "@islandflow/types";
import { import {
getSubscriptionKey as getLiveSubscriptionKey, getSubscriptionKey as getLiveSubscriptionKey,
@ -239,6 +241,7 @@ type MessageType =
| "equity-candle" | "equity-candle"
| "equity-join" | "equity-join"
| "flow-packet" | "flow-packet"
| "smart-money"
| "inferred-dark" | "inferred-dark"
| "classifier-hit" | "classifier-hit"
| "alert"; | "alert";
@ -1006,6 +1009,7 @@ const LIVE_SNAPSHOT_HISTORY_CHANNELS = new Set<LiveSubscription["channel"]>([
"nbbo", "nbbo",
"equities", "equities",
"flow", "flow",
"smart-money",
"classifier-hits" "classifier-hits"
]); ]);
@ -1052,12 +1056,22 @@ const classifyNbboSide = (price: number, quote: OptionNBBO | null | undefined):
}; };
type ClassifierDecor = { type ClassifierDecor = {
hit: ClassifierHitEvent; hit?: ClassifierHitEvent;
smartMoney?: SmartMoneyEvent;
family: string; family: string;
tone: string; tone: string;
intensity: number; intensity: number;
}; };
const SMART_MONEY_PROFILE_TONES: Record<SmartMoneyProfileId, string> = {
institutional_directional: "green",
retail_whale: "amber",
event_driven: "blue",
vol_seller: "copper",
arbitrage: "teal",
hedge_reactive: "magenta"
};
const CLASSIFIER_FAMILY_TONES: Record<string, string> = { const CLASSIFIER_FAMILY_TONES: Record<string, string> = {
large_bullish_call_sweep: "green", large_bullish_call_sweep: "green",
large_bearish_put_sweep: "red", large_bearish_put_sweep: "red",
@ -1095,6 +1109,12 @@ export const selectPrimaryClassifierHit = (
export const classifierToneForFamily = (classifierId: string): string => export const classifierToneForFamily = (classifierId: string): string =>
CLASSIFIER_FAMILY_TONES[classifierId] ?? "neutral"; 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 => ({ const buildClassifierDecor = (hit: ClassifierHitEvent): ClassifierDecor => ({
hit, hit,
family: hit.classifier_id, family: hit.classifier_id,
@ -1102,6 +1122,18 @@ const buildClassifierDecor = (hit: ClassifierHitEvent): ClassifierDecor => ({
intensity: clamp(hit.confidence, 0.25, 1) 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 = ( export const getOptionTableSnapshot = (
print: Pick< print: Pick<
OptionPrint, OptionPrint,
@ -2230,6 +2262,7 @@ type LiveSessionState = {
equityQuotes: EquityQuote[]; equityQuotes: EquityQuote[];
equityJoins: EquityPrintJoin[]; equityJoins: EquityPrintJoin[];
flow: FlowPacket[]; flow: FlowPacket[];
smartMoney: SmartMoneyEvent[];
classifierHits: ClassifierHitEvent[]; classifierHits: ClassifierHitEvent[];
alerts: AlertEvent[]; alerts: AlertEvent[];
inferredDark: InferredDarkEvent[]; inferredDark: InferredDarkEvent[];
@ -2249,6 +2282,7 @@ const LIVE_HISTORY_ENDPOINTS: Partial<Record<LiveSubscription["channel"], string
"equity-quotes": "/history/equity-quotes", "equity-quotes": "/history/equity-quotes",
"equity-joins": "/history/equity-joins", "equity-joins": "/history/equity-joins",
flow: "/history/flow", flow: "/history/flow",
"smart-money": "/history/smart-money",
"classifier-hits": "/history/classifier-hits", "classifier-hits": "/history/classifier-hits",
alerts: "/history/alerts", alerts: "/history/alerts",
"inferred-dark": "/history/inferred-dark" "inferred-dark": "/history/inferred-dark"
@ -2318,6 +2352,7 @@ export const getLiveManifest = (
{ channel: "nbbo" }, { channel: "nbbo" },
{ channel: "equities", ...equityScope }, { channel: "equities", ...equityScope },
{ channel: "flow", filters: flowFilters }, { channel: "flow", filters: flowFilters },
{ channel: "smart-money" },
{ channel: "classifier-hits" } { channel: "classifier-hits" }
]); ]);
} }
@ -2327,6 +2362,7 @@ export const getLiveManifest = (
{ channel: "equities", ...equityScope }, { channel: "equities", ...equityScope },
{ channel: "flow", filters: flowFilters }, { channel: "flow", filters: flowFilters },
{ channel: "alerts" }, { channel: "alerts" },
{ channel: "smart-money" },
{ channel: "classifier-hits" }, { channel: "classifier-hits" },
{ channel: "inferred-dark" }, { channel: "inferred-dark" },
...chartSubs ...chartSubs
@ -2357,6 +2393,7 @@ const useLiveSession = (
const [equityQuotes, setEquityQuotes] = useState<EquityQuote[]>([]); const [equityQuotes, setEquityQuotes] = useState<EquityQuote[]>([]);
const [equityJoins, setEquityJoins] = useState<EquityPrintJoin[]>([]); const [equityJoins, setEquityJoins] = useState<EquityPrintJoin[]>([]);
const [flow, setFlow] = useState<FlowPacket[]>([]); const [flow, setFlow] = useState<FlowPacket[]>([]);
const [smartMoney, setSmartMoney] = useState<SmartMoneyEvent[]>([]);
const [classifierHits, setClassifierHits] = useState<ClassifierHitEvent[]>([]); const [classifierHits, setClassifierHits] = useState<ClassifierHitEvent[]>([]);
const [alerts, setAlerts] = useState<AlertEvent[]>([]); const [alerts, setAlerts] = useState<AlertEvent[]>([]);
const [inferredDark, setInferredDark] = useState<InferredDarkEvent[]>([]); const [inferredDark, setInferredDark] = useState<InferredDarkEvent[]>([]);
@ -2389,6 +2426,7 @@ const useLiveSession = (
setEquityQuotes([]); setEquityQuotes([]);
setEquityJoins([]); setEquityJoins([]);
setFlow([]); setFlow([]);
setSmartMoney([]);
setClassifierHits([]); setClassifierHits([]);
setAlerts([]); setAlerts([]);
setInferredDark([]); setInferredDark([]);
@ -2489,6 +2527,9 @@ const useLiveSession = (
case "flow": case "flow":
mergeItems(setFlow, items as FlowPacket[]); mergeItems(setFlow, items as FlowPacket[]);
break; break;
case "smart-money":
mergeItems(setSmartMoney, items as SmartMoneyEvent[]);
break;
case "classifier-hits": case "classifier-hits":
mergeItems(setClassifierHits, items as ClassifierHitEvent[]); mergeItems(setClassifierHits, items as ClassifierHitEvent[]);
break; break;
@ -2757,6 +2798,9 @@ const useLiveSession = (
case "flow": case "flow":
mergeOlder(setFlow, LIVE_HOT_WINDOW); mergeOlder(setFlow, LIVE_HOT_WINDOW);
break; break;
case "smart-money":
mergeOlder(setSmartMoney, LIVE_HOT_WINDOW);
break;
case "classifier-hits": case "classifier-hits":
mergeOlder(setClassifierHits, LIVE_HOT_WINDOW); mergeOlder(setClassifierHits, LIVE_HOT_WINDOW);
break; break;
@ -2801,6 +2845,7 @@ const useLiveSession = (
equityQuotes, equityQuotes,
equityJoins, equityJoins,
flow, flow,
smartMoney,
classifierHits, classifierHits,
alerts, alerts,
inferredDark, inferredDark,
@ -2879,14 +2924,14 @@ type CandleChartProps = {
replayTime?: number | null; replayTime?: number | null;
liveCandles?: EquityCandle[]; liveCandles?: EquityCandle[];
liveOverlayPrints?: EquityPrint[]; liveOverlayPrints?: EquityPrint[];
classifierHits: ClassifierHitEvent[]; smartMoneyEvents: SmartMoneyEvent[];
inferredDark: InferredDarkEvent[]; inferredDark: InferredDarkEvent[];
onClassifierHitClick: (hit: ClassifierHitEvent) => void; onSmartMoneyClick: (event: SmartMoneyEvent) => void;
onInferredDarkClick: (event: InferredDarkEvent) => void; onInferredDarkClick: (event: InferredDarkEvent) => void;
}; };
type MarkerAction = type MarkerAction =
| { kind: "hit"; hit: ClassifierHitEvent } | { kind: "smart-money"; event: SmartMoneyEvent }
| { kind: "dark"; event: InferredDarkEvent }; | { kind: "dark"; event: InferredDarkEvent };
const CandleChart = ({ const CandleChart = ({
@ -2896,9 +2941,9 @@ const CandleChart = ({
replayTime = null, replayTime = null,
liveCandles = [], liveCandles = [],
liveOverlayPrints = [], liveOverlayPrints = [],
classifierHits, smartMoneyEvents,
inferredDark, inferredDark,
onClassifierHitClick, onSmartMoneyClick,
onInferredDarkClick onInferredDarkClick
}: CandleChartProps) => { }: CandleChartProps) => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@ -2912,7 +2957,7 @@ const CandleChart = ({
const markerLookupRef = useRef<Map<string, MarkerAction>>(new Map()); const markerLookupRef = useRef<Map<string, MarkerAction>>(new Map());
const [visibleRangeMs, setVisibleRangeMs] = useState<{ from: number; to: number } | null>(null); const [visibleRangeMs, setVisibleRangeMs] = useState<{ from: number; to: number } | null>(null);
const onHitClickRef = useRef(onClassifierHitClick); const onSmartMoneyClickRef = useRef(onSmartMoneyClick);
const onDarkClickRef = useRef(onInferredDarkClick); const onDarkClickRef = useRef(onInferredDarkClick);
const overlayCanvasRef = useRef<HTMLCanvasElement | null>(null); const overlayCanvasRef = useRef<HTMLCanvasElement | null>(null);
@ -2990,8 +3035,8 @@ const CandleChart = ({
}, [drawOverlay, ticker, intervalMs, mode]); }, [drawOverlay, ticker, intervalMs, mode]);
useEffect(() => { useEffect(() => {
onHitClickRef.current = onClassifierHitClick; onSmartMoneyClickRef.current = onSmartMoneyClick;
}, [onClassifierHitClick]); }, [onSmartMoneyClick]);
useEffect(() => { useEffect(() => {
onDarkClickRef.current = onInferredDarkClick; onDarkClickRef.current = onInferredDarkClick;
@ -3006,8 +3051,8 @@ const CandleChart = ({
} }
const { from, to } = visibleRangeMs; const { from, to } = visibleRangeMs;
const inRangeHits = classifierHits const inRangeSmartMoney = smartMoneyEvents
.filter((hit) => hit.source_ts >= from && hit.source_ts <= to) .filter((event) => event.source_ts >= from && event.source_ts <= to)
.sort((a, b) => { .sort((a, b) => {
const delta = a.source_ts - b.source_ts; const delta = a.source_ts - b.source_ts;
if (delta !== 0) { if (delta !== 0) {
@ -3025,27 +3070,27 @@ const CandleChart = ({
return a.seq - b.seq; return a.seq - b.seq;
}); });
const MAX_HIT_MARKERS = 220; const MAX_SMART_MONEY_MARKERS = 220;
const MAX_DARK_MARKERS = 120; const MAX_DARK_MARKERS = 120;
const MAX_TOTAL_MARKERS = 320; const MAX_TOTAL_MARKERS = 320;
const cappedHits = const cappedSmartMoney =
inRangeHits.length > MAX_HIT_MARKERS inRangeSmartMoney.length > MAX_SMART_MONEY_MARKERS
? inRangeHits.slice(inRangeHits.length - MAX_HIT_MARKERS) ? inRangeSmartMoney.slice(inRangeSmartMoney.length - MAX_SMART_MONEY_MARKERS)
: inRangeHits; : inRangeSmartMoney;
const cappedDark = const cappedDark =
inRangeDark.length > MAX_DARK_MARKERS inRangeDark.length > MAX_DARK_MARKERS
? inRangeDark.slice(inRangeDark.length - MAX_DARK_MARKERS) ? inRangeDark.slice(inRangeDark.length - MAX_DARK_MARKERS)
: inRangeDark; : inRangeDark;
for (const hit of cappedHits) { for (const event of cappedSmartMoney) {
const direction = normalizeDirection(hit.direction); const direction = normalizeDirection(event.primary_direction);
const markerId = `hit:${hit.trace_id}:${hit.seq}`; const markerId = `smart-money:${event.trace_id}:${event.seq}`;
lookup.set(markerId, { kind: "hit", hit }); lookup.set(markerId, { kind: "smart-money", event });
markers.push({ markers.push({
id: markerId, id: markerId,
time: toChartTime(hit.source_ts), time: toChartTime(event.source_ts),
position: direction === "bullish" ? "belowBar" : "aboveBar", position: direction === "bullish" ? "belowBar" : "aboveBar",
color: color:
direction === "bullish" direction === "bullish"
@ -3059,7 +3104,11 @@ const CandleChart = ({
: direction === "bearish" : direction === "bearish"
? "arrowDown" ? "arrowDown"
: "circle", : "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 }; return { markers: cappedMarkers, lookup };
}, [classifierHits, inferredDark, visibleRangeMs]); }, [smartMoneyEvents, inferredDark, visibleRangeMs]);
useEffect(() => { useEffect(() => {
if (!seriesRef.current) { if (!seriesRef.current) {
@ -3221,8 +3270,8 @@ const CandleChart = ({
if (!action) { if (!action) {
return; return;
} }
if (action.kind === "hit") { if (action.kind === "smart-money") {
onHitClickRef.current(action.hit); onSmartMoneyClickRef.current(action.event);
} else { } else {
onDarkClickRef.current(action.event); 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 (
<aside className="drawer">
<div className="drawer-header">
<div>
<p className="drawer-eyebrow">Smart money profile</p>
<h3>{smartMoneyProfileLabel(event.primary_profile_id)}</h3>
<p className="drawer-subtitle">{formatDateTime(event.source_ts)}</p>
</div>
<button className="drawer-close" type="button" onClick={onClose}>
Close
</button>
</div>
<div className="drawer-meta">
<span className={`pill direction-${direction}`}>{direction}</span>
<span className="drawer-chip">
Probability {primaryScore ? formatConfidence(primaryScore.probability) : "--"}
</span>
{event.abstained ? <span className="drawer-chip">Abstained</span> : null}
</div>
<div className="drawer-section">
<h4>Profile ladder</h4>
<div className="drawer-list">
{event.profile_scores.slice(0, 6).map((score) => (
<div className="drawer-row" key={`${event.event_id}-${score.profile_id}`}>
<div className="drawer-row-title">{smartMoneyProfileLabel(score.profile_id)}</div>
<div className="drawer-row-meta">
<span className={`pill direction-${normalizeDirection(score.direction)}`}>
{normalizeDirection(score.direction)}
</span>
<span>{formatConfidence(score.probability)}</span>
<span>{score.confidence_band}</span>
</div>
{score.reasons[0] ? <p className="drawer-note">{score.reasons[0]}</p> : null}
</div>
))}
</div>
{event.suppressed_reasons.length > 0 ? (
<p className="drawer-empty">Suppressed: {event.suppressed_reasons.join(", ")}</p>
) : null}
</div>
<div className="drawer-section">
<h4>Parent event</h4>
<div className="drawer-row">
<div className="drawer-row-title">{event.underlying_id}</div>
<div className="drawer-row-meta">
<span>{formatFlowMetric(event.features.print_count)} prints</span>
<span>{formatFlowMetric(event.features.total_size)} size</span>
<span>${formatCompactUsd(event.features.total_premium)}</span>
</div>
<p className="drawer-note">
Window {formatFlowMetric(event.event_window_ms, "ms")} · {event.event_kind}
</p>
</div>
{flowPacket ? (
<p className="drawer-note">Flow packet {flowPacket.id}</p>
) : null}
</div>
<div className="drawer-section">
<h4>Evidence prints</h4>
{evidencePrints.length === 0 ? (
<p className="drawer-empty">No linked option prints in the live cache yet.</p>
) : (
<div className="drawer-list">
{evidencePrints.slice(0, 6).map((item) => (
<div className="drawer-row" key={item.id}>
<div className="drawer-row-title">{item.print.option_contract_id}</div>
<div className="drawer-row-meta">
<span>${formatPrice(item.print.price)}</span>
<span>{formatSize(item.print.size)}x</span>
<span>{item.print.exchange}</span>
</div>
<p className="drawer-note">{formatTime(item.print.ts)}</p>
</div>
))}
</div>
)}
{unknownCount > 0 ? (
<p className="drawer-empty">+{unknownCount} evidence prints not in cache.</p>
) : null}
</div>
</aside>
);
};
type DarkDrawerProps = { type DarkDrawerProps = {
event: InferredDarkEvent; event: InferredDarkEvent;
evidence: DarkEvidenceItem[]; evidence: DarkEvidenceItem[];
@ -4009,6 +4161,7 @@ const useTerminalState = () => {
const [selectedAlert, setSelectedAlert] = useState<AlertEvent | null>(null); const [selectedAlert, setSelectedAlert] = useState<AlertEvent | null>(null);
const [selectedDarkEvent, setSelectedDarkEvent] = useState<InferredDarkEvent | null>(null); const [selectedDarkEvent, setSelectedDarkEvent] = useState<InferredDarkEvent | null>(null);
const [selectedClassifierHit, setSelectedClassifierHit] = useState<ClassifierHitEvent | null>(null); const [selectedClassifierHit, setSelectedClassifierHit] = useState<ClassifierHitEvent | null>(null);
const [selectedSmartMoneyEvent, setSelectedSmartMoneyEvent] = useState<SmartMoneyEvent | null>(null);
const [selectedInstrument, setSelectedInstrument] = useState<SelectedInstrument>(null); const [selectedInstrument, setSelectedInstrument] = useState<SelectedInstrument>(null);
const [filterInput, setFilterInput] = useState<string>(""); const [filterInput, setFilterInput] = useState<string>("");
const [flowFilters, setFlowFilters] = useState<OptionFlowFilters>(() => buildDefaultFlowFilters()); const [flowFilters, setFlowFilters] = useState<OptionFlowFilters>(() => buildDefaultFlowFilters());
@ -4078,13 +4231,14 @@ const useTerminalState = () => {
}, [mode]); }, [mode]);
useEffect(() => { useEffect(() => {
if (!selectedAlert && !selectedClassifierHit && !selectedDarkEvent) { if (!selectedAlert && !selectedClassifierHit && !selectedDarkEvent && !selectedSmartMoneyEvent) {
return; return;
} }
const dismissDrawers = () => { const dismissDrawers = () => {
setSelectedAlert(null); setSelectedAlert(null);
setSelectedClassifierHit(null); setSelectedClassifierHit(null);
setSelectedSmartMoneyEvent(null);
setSelectedDarkEvent(null); setSelectedDarkEvent(null);
}; };
@ -4108,7 +4262,7 @@ const useTerminalState = () => {
document.removeEventListener("mousedown", handlePointerDown); document.removeEventListener("mousedown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("keydown", handleKeyDown);
}; };
}, [selectedAlert, selectedClassifierHit, selectedDarkEvent]); }, [selectedAlert, selectedClassifierHit, selectedDarkEvent, selectedSmartMoneyEvent]);
const optionsScroll = useListScroll(); const optionsScroll = useListScroll();
const equitiesScroll = useListScroll(); const equitiesScroll = useListScroll();
@ -4250,6 +4404,19 @@ const useTerminalState = () => {
onNewItems: classifierScroll.onNewItems, onNewItems: classifierScroll.onNewItems,
getReplayKey: disableReplayGrouping getReplayKey: disableReplayGrouping
}); });
const smartMoney = useTape<SmartMoneyEvent>({
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<OptionPrint>({ const liveOptions = usePausableTapeView<OptionPrint>({
enabled: mode === "live", enabled: mode === "live",
@ -4302,6 +4469,10 @@ const useTerminalState = () => {
mode === "live" mode === "live"
? toStaticTapeState(liveSession.status, liveSession.classifierHits, liveSession.lastUpdate) ? toStaticTapeState(liveSession.status, liveSession.classifierHits, liveSession.lastUpdate)
: classifierHits; : classifierHits;
const smartMoneyFeed =
mode === "live"
? toStaticTapeState(liveSession.status, liveSession.smartMoney, liveSession.lastUpdate)
: smartMoney;
const inferredDarkFeed = const inferredDarkFeed =
mode === "live" mode === "live"
? toStaticTapeState(liveSession.status, liveSession.inferredDark, liveSession.lastUpdate) ? toStaticTapeState(liveSession.status, liveSession.inferredDark, liveSession.lastUpdate)
@ -4329,7 +4500,7 @@ const useTerminalState = () => {
useLayoutEffect(() => { useLayoutEffect(() => {
classifierAnchor.apply(); classifierAnchor.apply();
}, [classifierHitsFeed.items, classifierAnchor.apply]); }, [smartMoneyFeed.items, classifierHitsFeed.items, classifierAnchor.apply]);
const nbboMap = useMemo(() => { const nbboMap = useMemo(() => {
const map = new Map<string, OptionNBBO>(); const map = new Map<string, OptionNBBO>();
@ -4595,6 +4766,7 @@ const useTerminalState = () => {
} }
setSelectedDarkEvent(null); setSelectedDarkEvent(null);
setSelectedClassifierHit(null); setSelectedClassifierHit(null);
setSelectedSmartMoneyEvent(null);
}, [mode]); }, [mode]);
const extractPacketContract = useCallback((packet: FlowPacket): string => { const extractPacketContract = useCallback((packet: FlowPacket): string => {
@ -4634,6 +4806,19 @@ const useTerminalState = () => {
return map; return map;
}, [classifierHitsFeed.items, extractPacketIdFromClassifierHitTrace]); }, [classifierHitsFeed.items, extractPacketIdFromClassifierHitTrace]);
const smartMoneyByPacketId = useMemo(() => {
const map = new Map<string, SmartMoneyEvent>();
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 packetIdByOptionTraceId = useMemo(() => {
const map = new Map<string, string>(); const map = new Map<string, string>();
for (const packet of flowFeed.items) { for (const packet of flowFeed.items) {
@ -4647,13 +4832,18 @@ const useTerminalState = () => {
const classifierDecorByOptionTraceId = useMemo(() => { const classifierDecorByOptionTraceId = useMemo(() => {
const map = new Map<string, ClassifierDecor>(); const map = new Map<string, ClassifierDecor>();
for (const [traceId, packetId] of packetIdByOptionTraceId) { 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) ?? []); const primary = selectPrimaryClassifierHit(classifierHitsByPacketId.get(packetId) ?? []);
if (primary) { if (primary) {
map.set(traceId, buildClassifierDecor(primary)); map.set(traceId, buildClassifierDecor(primary));
} }
} }
return map; return map;
}, [classifierHitsByPacketId, packetIdByOptionTraceId]); }, [classifierHitsByPacketId, packetIdByOptionTraceId, smartMoneyByPacketId]);
const selectedClassifierPacketId = useMemo(() => { const selectedClassifierPacketId = useMemo(() => {
if (!selectedClassifierHit) { if (!selectedClassifierHit) {
@ -4721,6 +4911,90 @@ const useTerminalState = () => {
}); });
}, [resolvedFlowPacketMap, resolvedOptionPrintMap, selectedClassifierHit, selectedClassifierPacketId]); }, [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<string, FlowPacket>();
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<string, OptionPrint>();
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( const inferAlertUnderlying = useCallback(
(alert: AlertEvent): string | null => { (alert: AlertEvent): string | null => {
const fromTrace = extractUnderlyingFromTrace(alert.trace_id); const fromTrace = extractUnderlyingFromTrace(alert.trace_id);
@ -4932,6 +5206,9 @@ const useTerminalState = () => {
if (selectedClassifierPacketId) { if (selectedClassifierPacketId) {
keys.add(selectedClassifierPacketId); keys.add(selectedClassifierPacketId);
} }
for (const packetId of selectedSmartMoneyEvent?.packet_ids ?? []) {
keys.add(packetId);
}
for (const alert of visibleAlerts) { for (const alert of visibleAlerts) {
const packetId = alert.evidence_refs[0]; const packetId = alert.evidence_refs[0];
if (packetId) { if (packetId) {
@ -4939,7 +5216,7 @@ const useTerminalState = () => {
} }
} }
return keys; return keys;
}, [selectedAlert, selectedClassifierPacketId, visibleAlerts]); }, [selectedAlert, selectedClassifierPacketId, selectedSmartMoneyEvent, visibleAlerts]);
const activePinnedOptionKeys = useMemo(() => { const activePinnedOptionKeys = useMemo(() => {
const keys = new Set<string>(); const keys = new Set<string>();
@ -4953,11 +5230,14 @@ const useTerminalState = () => {
keys.add(id); keys.add(id);
} }
} }
for (const id of selectedSmartMoneyEvent?.member_print_ids ?? []) {
keys.add(id);
}
for (const id of visibleAlertEvidenceRefs) { for (const id of visibleAlertEvidenceRefs) {
keys.add(id); keys.add(id);
} }
return keys; return keys;
}, [selectedAlert, selectedClassifierFlowPacket, visibleAlertEvidenceRefs]); }, [selectedAlert, selectedClassifierFlowPacket, selectedSmartMoneyEvent, visibleAlertEvidenceRefs]);
const activePinnedJoinKeys = useMemo(() => { const activePinnedJoinKeys = useMemo(() => {
const keys = new Set<string>(); const keys = new Set<string>();
@ -5009,10 +5289,17 @@ const useTerminalState = () => {
}); });
}, [classifierHitsFeed.items, extractUnderlyingFromTrace, matchesTicker, tickerSet]); }, [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(); const desired = chartTicker.toUpperCase();
return classifierHitsFeed.items return smartMoneyFeed.items
.filter((hit) => extractUnderlyingFromTrace(hit.trace_id) === desired) .filter((event) => event.underlying_id.toUpperCase() === desired)
.sort((a, b) => { .sort((a, b) => {
const delta = a.source_ts - b.source_ts; const delta = a.source_ts - b.source_ts;
if (delta !== 0) { if (delta !== 0) {
@ -5020,7 +5307,7 @@ const useTerminalState = () => {
} }
return a.seq - b.seq; return a.seq - b.seq;
}); });
}, [chartTicker, classifierHitsFeed.items, extractUnderlyingFromTrace]); }, [chartTicker, smartMoneyFeed.items]);
const chartInferredDark = useMemo(() => { const chartInferredDark = useMemo(() => {
const desired = chartTicker.toUpperCase(); const desired = chartTicker.toUpperCase();
@ -5058,27 +5345,37 @@ const useTerminalState = () => {
if (alert) { if (alert) {
setSelectedClassifierHit(null); setSelectedClassifierHit(null);
setSelectedDarkEvent(null); setSelectedDarkEvent(null);
setSelectedSmartMoneyEvent(null);
setSelectedAlert(alert); setSelectedAlert(alert);
return; return;
} }
setSelectedAlert(null); setSelectedAlert(null);
setSelectedDarkEvent(null); setSelectedDarkEvent(null);
setSelectedSmartMoneyEvent(null);
setSelectedClassifierHit(hit); setSelectedClassifierHit(hit);
}, },
[findAlertForClassifierHit] [findAlertForClassifierHit]
); );
const handleClassifierMarkerClick = useCallback( const openFromSmartMoneyEvent = useCallback((event: SmartMoneyEvent) => {
(hit: ClassifierHitEvent) => { setSelectedAlert(null);
openFromClassifierHit(hit); setSelectedClassifierHit(null);
setSelectedDarkEvent(null);
setSelectedSmartMoneyEvent(event);
}, []);
const handleSmartMoneyMarkerClick = useCallback(
(event: SmartMoneyEvent) => {
openFromSmartMoneyEvent(event);
}, },
[openFromClassifierHit] [openFromSmartMoneyEvent]
); );
const handleDarkMarkerClick = useCallback((event: InferredDarkEvent) => { const handleDarkMarkerClick = useCallback((event: InferredDarkEvent) => {
setSelectedAlert(null); setSelectedAlert(null);
setSelectedClassifierHit(null); setSelectedClassifierHit(null);
setSelectedSmartMoneyEvent(null);
setSelectedDarkEvent(event); setSelectedDarkEvent(event);
}, []); }, []);
@ -5089,6 +5386,7 @@ const useTerminalState = () => {
inferredDarkFeed.lastUpdate, inferredDarkFeed.lastUpdate,
flowFeed.lastUpdate, flowFeed.lastUpdate,
alertsFeed.lastUpdate, alertsFeed.lastUpdate,
smartMoneyFeed.lastUpdate,
classifierHitsFeed.lastUpdate classifierHitsFeed.lastUpdate
] ]
.filter((value): value is number => value !== null) .filter((value): value is number => value !== null)
@ -5099,6 +5397,7 @@ const useTerminalState = () => {
inferredDarkFeed.lastUpdate, inferredDarkFeed.lastUpdate,
flowFeed.lastUpdate, flowFeed.lastUpdate,
alertsFeed.lastUpdate, alertsFeed.lastUpdate,
smartMoneyFeed.lastUpdate,
classifierHitsFeed.lastUpdate classifierHitsFeed.lastUpdate
]); ]);
@ -5113,6 +5412,8 @@ const useTerminalState = () => {
setSelectedDarkEvent, setSelectedDarkEvent,
selectedClassifierHit, selectedClassifierHit,
setSelectedClassifierHit, setSelectedClassifierHit,
selectedSmartMoneyEvent,
setSelectedSmartMoneyEvent,
selectedInstrument, selectedInstrument,
setSelectedInstrument, setSelectedInstrument,
selectedInstrumentLabel, selectedInstrumentLabel,
@ -5135,6 +5436,7 @@ const useTerminalState = () => {
inferredDark: inferredDarkFeed, inferredDark: inferredDarkFeed,
flow: flowFeed, flow: flowFeed,
alerts: alertsFeed, alerts: alertsFeed,
smartMoney: smartMoneyFeed,
classifierHits: classifierHitsFeed, classifierHits: classifierHitsFeed,
liveSession, liveSession,
activeTickers, activeTickers,
@ -5155,17 +5457,21 @@ const useTerminalState = () => {
selectedClassifierPacketId, selectedClassifierPacketId,
selectedClassifierFlowPacket, selectedClassifierFlowPacket,
selectedClassifierEvidence, selectedClassifierEvidence,
selectedSmartMoneyFlowPacket,
selectedSmartMoneyEvidence,
filteredOptions, filteredOptions,
filteredEquities, filteredEquities,
equitiesSilentWarning, equitiesSilentWarning,
filteredInferredDark, filteredInferredDark,
filteredFlow, filteredFlow,
filteredAlerts, filteredAlerts,
filteredSmartMoneyEvents,
filteredClassifierHits, filteredClassifierHits,
chartClassifierHits, chartSmartMoneyEvents,
chartInferredDark, chartInferredDark,
openFromSmartMoneyEvent,
openFromClassifierHit, openFromClassifierHit,
handleClassifierMarkerClick, handleSmartMoneyMarkerClick,
handleDarkMarkerClick, handleDarkMarkerClick,
lastSeen, lastSeen,
toggleMode: () => { toggleMode: () => {
@ -5618,12 +5924,22 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
type="button" type="button"
{...commonProps} {...commonProps}
key={`${print.trace_id}-${print.seq}`} 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) => { onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") { if (event.key === "Enter" || event.key === " ") {
event.preventDefault(); event.preventDefault();
if (decor.smartMoney) {
state.openFromSmartMoneyEvent(decor.smartMoney);
} else if (decor.hit) {
state.openFromClassifierHit(decor.hit); state.openFromClassifierHit(decor.hit);
} }
}
}} }}
> >
{cells} {cells}
@ -5951,6 +6267,7 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) =>
onClick={() => { onClick={() => {
state.setSelectedDarkEvent(null); state.setSelectedDarkEvent(null);
state.setSelectedClassifierHit(null); state.setSelectedClassifierHit(null);
state.setSelectedSmartMoneyEvent(null);
state.setSelectedAlert(alert); state.setSelectedAlert(alert);
}} }}
> >
@ -5982,8 +6299,22 @@ type ClassifierPaneProps = {
const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => { const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
const state = useTerminal(); const state = useTerminal();
const items = limit ? state.filteredClassifierHits.slice(0, limit) : state.filteredClassifierHits; const smartMoneyItems = limit ? state.filteredSmartMoneyEvents.slice(0, limit) : state.filteredSmartMoneyEvents;
const virtual = useVirtualList(items, state.classifierScroll.listRef, !limit, 44); const legacyItems =
smartMoneyItems.length === 0
? limit
? state.filteredClassifierHits.slice(0, limit)
: state.filteredClassifierHits
: [];
const items: Array<SmartMoneyEvent | ClassifierHitEvent> =
smartMoneyItems.length > 0 ? smartMoneyItems : legacyItems;
const virtual = useVirtualList<SmartMoneyEvent | ClassifierHitEvent>(
items,
state.classifierScroll.listRef,
!limit,
44
);
const showingSmartMoney = smartMoneyItems.length > 0;
return ( return (
<Pane <Pane
@ -5991,19 +6322,19 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
title="Rules" title="Rules"
status={ status={
<TapeStatus <TapeStatus
status={state.classifierHits.status} status={state.smartMoney.status}
lastUpdate={state.classifierHits.lastUpdate} lastUpdate={state.smartMoney.lastUpdate ?? state.classifierHits.lastUpdate}
replayTime={state.classifierHits.replayTime} replayTime={state.smartMoney.replayTime ?? state.classifierHits.replayTime}
replayComplete={state.classifierHits.replayComplete} replayComplete={state.smartMoney.replayComplete || state.classifierHits.replayComplete}
paused={state.classifierHits.paused} paused={state.smartMoney.paused}
dropped={state.classifierHits.dropped} dropped={state.smartMoney.dropped}
mode={state.mode} mode={state.mode}
/> />
} }
actions={ actions={
<TapeControls <TapeControls
paused={state.classifierHits.paused} paused={state.smartMoney.paused}
onTogglePause={state.classifierHits.togglePause} onTogglePause={state.smartMoney.togglePause}
isAtTop={state.classifierScroll.isAtTop} isAtTop={state.classifierScroll.isAtTop}
missed={state.classifierScroll.missed} missed={state.classifierScroll.missed}
onJump={state.classifierScroll.jumpToTop} onJump={state.classifierScroll.jumpToTop}
@ -6016,23 +6347,48 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
{state.tickerSet.size > 0 {state.tickerSet.size > 0
? "No classifier hits match the current filter." ? "No classifier hits match the current filter."
: state.mode === "live" : state.mode === "live"
? "No classifier hits yet. Start compute." ? "No smart-money profiles yet. Start compute."
: "Replay queue empty. Ensure ClickHouse has data."} : "Replay queue empty. Ensure ClickHouse has data."}
</div> </div>
) : ( ) : (
<div className="data-table-wrap" ref={state.classifierScroll.setListRef}> <div className="data-table-wrap" ref={state.classifierScroll.setListRef}>
<div className="data-table data-table-classifier" role="table" aria-label="Classifier hits"> <div className="data-table data-table-classifier" role="table" aria-label="Smart money profiles">
<div className="data-table-head" role="row"> <div className="data-table-head" role="row">
<span className="data-table-cell">TIME</span> <span className="data-table-cell">TIME</span>
<span className="data-table-cell">RULE</span> <span className="data-table-cell">PROFILE</span>
<span className="data-table-cell">DIR</span> <span className="data-table-cell">DIR</span>
<span className="data-table-cell">CONF</span> <span className="data-table-cell">PROB</span>
<span className="data-table-cell">NOTE</span> <span className="data-table-cell">NOTE</span>
</div> </div>
{virtual.topSpacerHeight > 0 ? ( {virtual.topSpacerHeight > 0 ? (
<div className="data-table-spacer" style={{ height: `${virtual.topSpacerHeight}px` }} aria-hidden /> <div className="data-table-spacer" style={{ height: `${virtual.topSpacerHeight}px` }} aria-hidden />
) : null} ) : null}
{virtual.visibleItems.map((hit) => { {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 (
<button
className={`data-table-row data-table-row-button data-table-row-classifier data-table-row-direction-${direction}`}
key={`${event.trace_id}-${event.seq}`}
type="button"
onClick={() => state.openFromSmartMoneyEvent(event)}
>
<span className="data-table-cell data-table-cell-number">{formatTime(event.source_ts)}</span>
<span className="data-table-cell">{smartMoneyProfileLabel(event.primary_profile_id)}</span>
<span className="data-table-cell">{direction}</span>
<span className="data-table-cell data-table-cell-number">
{primaryScore ? formatConfidence(primaryScore.probability) : "--"}
</span>
<span className="data-table-cell">
{event.abstained
? event.suppressed_reasons[0] ?? "abstained"
: primaryScore?.reasons[0] ?? primaryScore?.confidence_band ?? "--"}
</span>
</button>
);
}) : (virtual.visibleItems as ClassifierHitEvent[]).map((hit) => {
const direction = normalizeDirection(hit.direction); const direction = normalizeDirection(hit.direction);
return ( return (
<button <button
@ -6130,6 +6486,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => {
onClick={() => { onClick={() => {
state.setSelectedAlert(null); state.setSelectedAlert(null);
state.setSelectedClassifierHit(null); state.setSelectedClassifierHit(null);
state.setSelectedSmartMoneyEvent(null);
state.setSelectedDarkEvent(event); state.setSelectedDarkEvent(event);
}} }}
> >
@ -6188,9 +6545,9 @@ const ChartPane = ({ title = "Chart" }: ChartPaneProps) => {
replayTime={state.equities.replayTime} replayTime={state.equities.replayTime}
liveCandles={state.liveSession.chartCandles} liveCandles={state.liveSession.chartCandles}
liveOverlayPrints={state.liveSession.chartOverlay} liveOverlayPrints={state.liveSession.chartOverlay}
classifierHits={state.chartClassifierHits} smartMoneyEvents={state.chartSmartMoneyEvents}
inferredDark={state.chartInferredDark} inferredDark={state.chartInferredDark}
onClassifierHitClick={state.handleClassifierMarkerClick} onSmartMoneyClick={state.handleSmartMoneyMarkerClick}
onInferredDarkClick={state.handleDarkMarkerClick} onInferredDarkClick={state.handleDarkMarkerClick}
/> />
</Pane> </Pane>
@ -6199,7 +6556,7 @@ const ChartPane = ({ title = "Chart" }: ChartPaneProps) => {
const FocusPane = () => { const FocusPane = () => {
const state = useTerminal(); const state = useTerminal();
const hits = state.chartClassifierHits.slice(-10).reverse(); const hits = state.chartSmartMoneyEvents.slice(-10).reverse();
const dark = state.chartInferredDark.slice(-10).reverse(); const dark = state.chartInferredDark.slice(-10).reverse();
return ( return (
@ -6220,13 +6577,13 @@ const FocusPane = () => {
className="row row-button" className="row row-button"
key={`${hit.trace_id}-${hit.seq}`} key={`${hit.trace_id}-${hit.seq}`}
type="button" type="button"
onClick={() => state.openFromClassifierHit(hit)} onClick={() => state.openFromSmartMoneyEvent(hit)}
> >
<div> <div>
<div className="contract">{humanizeClassifierId(hit.classifier_id)}</div> <div className="contract">{smartMoneyProfileLabel(hit.primary_profile_id)}</div>
<div className="meta"> <div className="meta">
<span className={`pill direction-${normalizeDirection(hit.direction)}`}> <span className={`pill direction-${normalizeDirection(hit.primary_direction)}`}>
{normalizeDirection(hit.direction)} {normalizeDirection(hit.primary_direction)}
</span> </span>
<span>{formatTime(hit.source_ts)}</span> <span>{formatTime(hit.source_ts)}</span>
</div> </div>
@ -6396,6 +6753,15 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
/> />
) : null} ) : null}
{state.selectedSmartMoneyEvent ? (
<SmartMoneyDrawer
event={state.selectedSmartMoneyEvent}
flowPacket={state.selectedSmartMoneyFlowPacket}
evidence={state.selectedSmartMoneyEvidence}
onClose={() => state.setSelectedSmartMoneyEvent(null)}
/>
) : null}
{state.selectedDarkEvent ? ( {state.selectedDarkEvent ? (
<DarkDrawer <DarkDrawer
event={state.selectedDarkEvent} event={state.selectedDarkEvent}