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-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}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue