Migrate terminal to smart-money profiles
This commit is contained in:
parent
86661df7ae
commit
de6d25f046
4 changed files with 452 additions and 75 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<LiveSubscription["channel"]>([
|
|||
"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<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> = {
|
||||
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<Record<LiveSubscription["channel"], string
|
|||
"equity-quotes": "/history/equity-quotes",
|
||||
"equity-joins": "/history/equity-joins",
|
||||
flow: "/history/flow",
|
||||
"smart-money": "/history/smart-money",
|
||||
"classifier-hits": "/history/classifier-hits",
|
||||
alerts: "/history/alerts",
|
||||
"inferred-dark": "/history/inferred-dark"
|
||||
|
|
@ -2318,6 +2352,7 @@ export const getLiveManifest = (
|
|||
{ channel: "nbbo" },
|
||||
{ channel: "equities", ...equityScope },
|
||||
{ channel: "flow", filters: flowFilters },
|
||||
{ channel: "smart-money" },
|
||||
{ channel: "classifier-hits" }
|
||||
]);
|
||||
}
|
||||
|
|
@ -2327,6 +2362,7 @@ export const getLiveManifest = (
|
|||
{ channel: "equities", ...equityScope },
|
||||
{ channel: "flow", filters: flowFilters },
|
||||
{ channel: "alerts" },
|
||||
{ channel: "smart-money" },
|
||||
{ channel: "classifier-hits" },
|
||||
{ channel: "inferred-dark" },
|
||||
...chartSubs
|
||||
|
|
@ -2357,6 +2393,7 @@ const useLiveSession = (
|
|||
const [equityQuotes, setEquityQuotes] = useState<EquityQuote[]>([]);
|
||||
const [equityJoins, setEquityJoins] = useState<EquityPrintJoin[]>([]);
|
||||
const [flow, setFlow] = useState<FlowPacket[]>([]);
|
||||
const [smartMoney, setSmartMoney] = useState<SmartMoneyEvent[]>([]);
|
||||
const [classifierHits, setClassifierHits] = useState<ClassifierHitEvent[]>([]);
|
||||
const [alerts, setAlerts] = useState<AlertEvent[]>([]);
|
||||
const [inferredDark, setInferredDark] = useState<InferredDarkEvent[]>([]);
|
||||
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
|
|
@ -2912,7 +2957,7 @@ const CandleChart = ({
|
|||
|
||||
const markerLookupRef = useRef<Map<string, MarkerAction>>(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<HTMLCanvasElement | null>(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 (
|
||||
<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 = {
|
||||
event: InferredDarkEvent;
|
||||
evidence: DarkEvidenceItem[];
|
||||
|
|
@ -4009,6 +4161,7 @@ const useTerminalState = () => {
|
|||
const [selectedAlert, setSelectedAlert] = useState<AlertEvent | null>(null);
|
||||
const [selectedDarkEvent, setSelectedDarkEvent] = useState<InferredDarkEvent | null>(null);
|
||||
const [selectedClassifierHit, setSelectedClassifierHit] = useState<ClassifierHitEvent | null>(null);
|
||||
const [selectedSmartMoneyEvent, setSelectedSmartMoneyEvent] = useState<SmartMoneyEvent | null>(null);
|
||||
const [selectedInstrument, setSelectedInstrument] = useState<SelectedInstrument>(null);
|
||||
const [filterInput, setFilterInput] = useState<string>("");
|
||||
const [flowFilters, setFlowFilters] = useState<OptionFlowFilters>(() => 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<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>({
|
||||
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<string, OptionNBBO>();
|
||||
|
|
@ -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<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 map = new Map<string, string>();
|
||||
for (const packet of flowFeed.items) {
|
||||
|
|
@ -4647,13 +4832,18 @@ const useTerminalState = () => {
|
|||
const classifierDecorByOptionTraceId = useMemo(() => {
|
||||
const map = new Map<string, ClassifierDecor>();
|
||||
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<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(
|
||||
(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<string>();
|
||||
|
|
@ -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<string>();
|
||||
|
|
@ -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<SmartMoneyEvent | ClassifierHitEvent> =
|
||||
smartMoneyItems.length > 0 ? smartMoneyItems : legacyItems;
|
||||
const virtual = useVirtualList<SmartMoneyEvent | ClassifierHitEvent>(
|
||||
items,
|
||||
state.classifierScroll.listRef,
|
||||
!limit,
|
||||
44
|
||||
);
|
||||
const showingSmartMoney = smartMoneyItems.length > 0;
|
||||
|
||||
return (
|
||||
<Pane
|
||||
|
|
@ -5991,19 +6322,19 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
|
|||
title="Rules"
|
||||
status={
|
||||
<TapeStatus
|
||||
status={state.classifierHits.status}
|
||||
lastUpdate={state.classifierHits.lastUpdate}
|
||||
replayTime={state.classifierHits.replayTime}
|
||||
replayComplete={state.classifierHits.replayComplete}
|
||||
paused={state.classifierHits.paused}
|
||||
dropped={state.classifierHits.dropped}
|
||||
status={state.smartMoney.status}
|
||||
lastUpdate={state.smartMoney.lastUpdate ?? state.classifierHits.lastUpdate}
|
||||
replayTime={state.smartMoney.replayTime ?? state.classifierHits.replayTime}
|
||||
replayComplete={state.smartMoney.replayComplete || state.classifierHits.replayComplete}
|
||||
paused={state.smartMoney.paused}
|
||||
dropped={state.smartMoney.dropped}
|
||||
mode={state.mode}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
paused={state.classifierHits.paused}
|
||||
onTogglePause={state.classifierHits.togglePause}
|
||||
paused={state.smartMoney.paused}
|
||||
onTogglePause={state.smartMoney.togglePause}
|
||||
isAtTop={state.classifierScroll.isAtTop}
|
||||
missed={state.classifierScroll.missed}
|
||||
onJump={state.classifierScroll.jumpToTop}
|
||||
|
|
@ -6016,38 +6347,63 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
|
|||
{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."}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
<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">CONF</span>
|
||||
<span className="data-table-cell">PROB</span>
|
||||
<span className="data-table-cell">NOTE</span>
|
||||
</div>
|
||||
{virtual.topSpacerHeight > 0 ? (
|
||||
<div className="data-table-spacer" style={{ height: `${virtual.topSpacerHeight}px` }} aria-hidden />
|
||||
) : 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 (
|
||||
<button
|
||||
className={`data-table-row data-table-row-button data-table-row-classifier data-table-row-direction-${direction}`}
|
||||
key={`${hit.trace_id}-${hit.seq}`}
|
||||
key={`${event.trace_id}-${event.seq}`}
|
||||
type="button"
|
||||
onClick={() => state.openFromClassifierHit(hit)}
|
||||
onClick={() => state.openFromSmartMoneyEvent(event)}
|
||||
>
|
||||
<span className="data-table-cell data-table-cell-number">{formatTime(hit.source_ts)}</span>
|
||||
<span className="data-table-cell">{humanizeClassifierId(hit.classifier_id)}</span>
|
||||
<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">{formatConfidence(hit.confidence)}</span>
|
||||
<span className="data-table-cell">{hit.explanations?.[0] ?? "--"}</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);
|
||||
return (
|
||||
<button
|
||||
className={`data-table-row data-table-row-button data-table-row-classifier data-table-row-direction-${direction}`}
|
||||
key={`${hit.trace_id}-${hit.seq}`}
|
||||
type="button"
|
||||
onClick={() => state.openFromClassifierHit(hit)}
|
||||
>
|
||||
<span className="data-table-cell data-table-cell-number">{formatTime(hit.source_ts)}</span>
|
||||
<span className="data-table-cell">{humanizeClassifierId(hit.classifier_id)}</span>
|
||||
<span className="data-table-cell">{direction}</span>
|
||||
<span className="data-table-cell data-table-cell-number">{formatConfidence(hit.confidence)}</span>
|
||||
<span className="data-table-cell">{hit.explanations?.[0] ?? "--"}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{virtual.bottomSpacerHeight > 0 ? (
|
||||
<div className="data-table-spacer" style={{ height: `${virtual.bottomSpacerHeight}px` }} aria-hidden />
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</Pane>
|
||||
|
|
@ -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)}
|
||||
>
|
||||
<div>
|
||||
<div className="contract">{humanizeClassifierId(hit.classifier_id)}</div>
|
||||
<div className="contract">{smartMoneyProfileLabel(hit.primary_profile_id)}</div>
|
||||
<div className="meta">
|
||||
<span className={`pill direction-${normalizeDirection(hit.direction)}`}>
|
||||
{normalizeDirection(hit.direction)}
|
||||
<span className={`pill direction-${normalizeDirection(hit.primary_direction)}`}>
|
||||
{normalizeDirection(hit.primary_direction)}
|
||||
</span>
|
||||
<span>{formatTime(hit.source_ts)}</span>
|
||||
</div>
|
||||
|
|
@ -6396,6 +6753,15 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{state.selectedSmartMoneyEvent ? (
|
||||
<SmartMoneyDrawer
|
||||
event={state.selectedSmartMoneyEvent}
|
||||
flowPacket={state.selectedSmartMoneyFlowPacket}
|
||||
evidence={state.selectedSmartMoneyEvidence}
|
||||
onClose={() => state.setSelectedSmartMoneyEvent(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{state.selectedDarkEvent ? (
|
||||
<DarkDrawer
|
||||
event={state.selectedDarkEvent}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue