@leroylabs/cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -7
- package/bin/leroy.mjs +89 -12
- package/package.json +6 -1
- package/src/args.mjs +48 -8
- package/src/client.mjs +24 -15
- package/src/colors.mjs +14 -0
- package/src/demo.mjs +144 -0
- package/src/format.mjs +427 -88
- package/src/live-ui.mjs +655 -0
package/src/live-ui.mjs
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
4
|
+
|
|
5
|
+
import { ansiForeground, LEROY_COLORS } from "./colors.mjs";
|
|
6
|
+
import { buildEvaluationViewModel } from "./format.mjs";
|
|
7
|
+
|
|
8
|
+
const COLORS = {
|
|
9
|
+
green: LEROY_COLORS.green,
|
|
10
|
+
red: LEROY_COLORS.red,
|
|
11
|
+
white: "white",
|
|
12
|
+
muted: LEROY_COLORS.neutral,
|
|
13
|
+
content: LEROY_COLORS.neutral,
|
|
14
|
+
neutral: LEROY_COLORS.neutral,
|
|
15
|
+
inactive: LEROY_COLORS.inactive,
|
|
16
|
+
border: LEROY_COLORS.inactive,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MONOCHROME = Object.fromEntries(Object.keys(COLORS).map((key) => [key, undefined]));
|
|
20
|
+
const MAX_CARD_WIDTH = 67;
|
|
21
|
+
const MIN_CARD_WIDTH = 64;
|
|
22
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
23
|
+
const LOADING_STEP_DELAY_MS = 110;
|
|
24
|
+
const STRATEGY_COUNT = 41;
|
|
25
|
+
const STRATEGY_BAR_COUNT = 20;
|
|
26
|
+
const STRATEGY_CELL_COUNT = STRATEGY_BAR_COUNT;
|
|
27
|
+
const TABLE_STYLE = {
|
|
28
|
+
"padding-left": 0,
|
|
29
|
+
"padding-right": 0,
|
|
30
|
+
head: [],
|
|
31
|
+
border: [],
|
|
32
|
+
};
|
|
33
|
+
const ANSI_CODES = {
|
|
34
|
+
white: 37,
|
|
35
|
+
green: `38;2;0;212;27`,
|
|
36
|
+
red: `38;2;242;86;47`,
|
|
37
|
+
muted: 90,
|
|
38
|
+
neutral: `38;2;143;149;145`,
|
|
39
|
+
inactive: `38;2;41;51;43`,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function tableChars() {
|
|
43
|
+
return {
|
|
44
|
+
top: "",
|
|
45
|
+
"top-mid": "",
|
|
46
|
+
"top-left": "",
|
|
47
|
+
"top-right": "",
|
|
48
|
+
bottom: "",
|
|
49
|
+
"bottom-mid": "",
|
|
50
|
+
"bottom-left": "",
|
|
51
|
+
"bottom-right": "",
|
|
52
|
+
left: "",
|
|
53
|
+
"left-mid": "",
|
|
54
|
+
mid: "",
|
|
55
|
+
"mid-mid": "",
|
|
56
|
+
right: "",
|
|
57
|
+
"right-mid": "",
|
|
58
|
+
middle: "",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function palette(colorEnabled) {
|
|
63
|
+
return colorEnabled ? COLORS : MONOCHROME;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function record(value) {
|
|
67
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function clamp(value, minimum, maximum) {
|
|
71
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function frameKey(response, view) {
|
|
75
|
+
const frame = record(response.current_market_frame);
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
sourceTime: view.frameSourceTime,
|
|
78
|
+
tapeId: frame.tape_id ?? frame.tapeId ?? null,
|
|
79
|
+
last: view.last,
|
|
80
|
+
status: view.status,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function numberLabel(value, digits = 2) {
|
|
85
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
86
|
+
? `${value > 0 ? "+" : value < 0 ? "−" : ""}${Math.abs(value).toFixed(digits)}%`
|
|
87
|
+
: "—";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rateLabel(value) {
|
|
91
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
92
|
+
return `${(value > 1 ? value : value * 100).toFixed(0)}%`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function bpsLabel(value) {
|
|
96
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
97
|
+
return `${value > 0 ? "+" : value < 0 ? "−" : ""}${Math.abs(value).toFixed(2)}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function strengthCellGlyph(active, colorEnabled) {
|
|
101
|
+
return colorEnabled ? "██" : active ? "■■" : "□□";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function signedPercent(value) {
|
|
105
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
106
|
+
? `${value > 0 ? "+" : value < 0 ? "−" : ""}${Math.abs(value).toFixed(2)}%`
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function absolutePercent(value) {
|
|
111
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
112
|
+
? `${Math.abs(value).toFixed(2)}%`
|
|
113
|
+
: null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function marketVolumeLabel(value) {
|
|
117
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
118
|
+
return `${value.toFixed(2)}x`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function metricTone(value) {
|
|
122
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value === 0) return "white";
|
|
123
|
+
return value > 0 ? "green" : "red";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function tableContent(value, tone, colorEnabled, bold = false) {
|
|
127
|
+
const text = String(value);
|
|
128
|
+
const code = ANSI_CODES[tone];
|
|
129
|
+
return colorEnabled && code
|
|
130
|
+
? `${bold ? "\u001b[1m" : ""}\u001b[${code}m${text}\u001b[39m${bold ? "\u001b[22m" : ""}`
|
|
131
|
+
: text;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function tableValue(value, tone, colorEnabled, { bold = false, hAlign = "left" } = {}) {
|
|
135
|
+
const content = tableContent(value, tone, colorEnabled, bold);
|
|
136
|
+
return { content, hAlign };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function visibleLength(value) {
|
|
140
|
+
return String(value).replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "").length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderTable(rows, colWidths, colorEnabled, { divider = true } = {}) {
|
|
144
|
+
const table = new Table({ chars: tableChars(), style: TABLE_STYLE, colWidths });
|
|
145
|
+
table.push(...rows);
|
|
146
|
+
const lines = table.toString().split("\n").map((line) => line.trimEnd());
|
|
147
|
+
if (!divider) return lines.join("\n");
|
|
148
|
+
const dividerLine = "─".repeat(Math.max(1, colWidths.reduce((total, width) => total + width, 0)));
|
|
149
|
+
const coloredDivider = colorEnabled
|
|
150
|
+
? `${ansiForeground(LEROY_COLORS.inactive)}${dividerLine}\u001b[39m`
|
|
151
|
+
: dividerLine;
|
|
152
|
+
return [lines[0], coloredDivider, ...lines.slice(1)].join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function tableColumnWidths(rows) {
|
|
156
|
+
return rows[0].map((_, columnIndex) => Math.max(
|
|
157
|
+
...rows.map((row) => visibleLength(row[columnIndex]?.content ?? "")),
|
|
158
|
+
) + 2);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hasMarketMetrics(setup) {
|
|
162
|
+
return [setup?.sessionReturnPct, setup?.volumeRatio, setup?.vwapDistancePct, setup?.atrPct]
|
|
163
|
+
.some((value) => typeof value === "number" && Number.isFinite(value));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function MarketSetupTable({ symbol, setup, market, colorEnabled }) {
|
|
167
|
+
const columns = [
|
|
168
|
+
{ symbol, setup },
|
|
169
|
+
...(hasMarketMetrics(market) ? [{ symbol: market.symbol, setup: market }] : []),
|
|
170
|
+
];
|
|
171
|
+
const rows = [
|
|
172
|
+
[
|
|
173
|
+
tableValue("", null, colorEnabled),
|
|
174
|
+
...columns.map((column) => tableValue(column.symbol, "muted", colorEnabled, { bold: true })),
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
tableValue("Session", "muted", colorEnabled, { bold: true }),
|
|
178
|
+
...columns.map((column) => tableValue(
|
|
179
|
+
signedPercent(column.setup.sessionReturnPct) ?? "—",
|
|
180
|
+
metricTone(column.setup.sessionReturnPct),
|
|
181
|
+
colorEnabled,
|
|
182
|
+
{ bold: true },
|
|
183
|
+
)),
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
tableValue("Volume", "muted", colorEnabled, { bold: true }),
|
|
187
|
+
...columns.map((column) => tableValue(marketVolumeLabel(column.setup.volumeRatio), "white", colorEnabled, { bold: true })),
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
tableValue("VWAP", "muted", colorEnabled, { bold: true }),
|
|
191
|
+
...columns.map((column) => tableValue(
|
|
192
|
+
signedPercent(column.setup.vwapDistancePct) ?? "—",
|
|
193
|
+
metricTone(column.setup.vwapDistancePct),
|
|
194
|
+
colorEnabled,
|
|
195
|
+
{ bold: true },
|
|
196
|
+
)),
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
tableValue("Typical Range", "muted", colorEnabled, { bold: true }),
|
|
200
|
+
...columns.map((column) => tableValue(absolutePercent(column.setup.atrPct) ?? "—", "white", colorEnabled, { bold: true })),
|
|
201
|
+
],
|
|
202
|
+
];
|
|
203
|
+
return React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
204
|
+
React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled)),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function strategyLabel(text, tone, alignment, width) {
|
|
209
|
+
return React.createElement(Box, { key: text, width: `${100 / width}%`, justifyContent: alignment },
|
|
210
|
+
React.createElement(Text, { color: tone }, text),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function proportionalSignalCells(segments, total) {
|
|
215
|
+
if (total <= 0) return [];
|
|
216
|
+
const allocations = segments.map((segment) => {
|
|
217
|
+
const exact = (segment.count / total) * STRATEGY_CELL_COUNT;
|
|
218
|
+
return { ...segment, exact, count: Math.floor(exact) };
|
|
219
|
+
});
|
|
220
|
+
for (const segment of allocations) {
|
|
221
|
+
if (segment.count === 0 && segment.exact > 0) segment.count = 1;
|
|
222
|
+
}
|
|
223
|
+
let remaining = STRATEGY_CELL_COUNT - allocations.reduce((sum, segment) => sum + segment.count, 0);
|
|
224
|
+
while (remaining > 0) {
|
|
225
|
+
const segment = allocations.reduce((best, candidate) => {
|
|
226
|
+
if (!best) return candidate;
|
|
227
|
+
return candidate.exact - candidate.count > best.exact - best.count ? candidate : best;
|
|
228
|
+
}, null);
|
|
229
|
+
segment.count += 1;
|
|
230
|
+
remaining -= 1;
|
|
231
|
+
}
|
|
232
|
+
return allocations.flatMap((segment) => Array.from({ length: segment.count }, () => segment.key));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function StrategyMix({ strategy, colorEnabled }) {
|
|
236
|
+
const colors = palette(colorEnabled);
|
|
237
|
+
const neutralCount = strategy.counts.neutral + strategy.counts.warming;
|
|
238
|
+
const segments = [
|
|
239
|
+
{ key: "buy", count: strategy.counts.buy, tone: colors.green },
|
|
240
|
+
{ key: "neutral", count: neutralCount, tone: colors.neutral },
|
|
241
|
+
{ key: "sell", count: strategy.counts.sell, tone: colors.red },
|
|
242
|
+
];
|
|
243
|
+
const total = segments.reduce((sum, segment) => sum + segment.count, 0);
|
|
244
|
+
const stateList = proportionalSignalCells(segments, total);
|
|
245
|
+
const toneByState = Object.fromEntries(segments.map((segment) => [segment.key, segment.tone]));
|
|
246
|
+
const paddedStates = [
|
|
247
|
+
...stateList,
|
|
248
|
+
...Array.from({ length: STRATEGY_CELL_COUNT - stateList.length }, () => null),
|
|
249
|
+
];
|
|
250
|
+
const rowWidth = STRATEGY_CELL_COUNT * 2 + STRATEGY_CELL_COUNT - 1;
|
|
251
|
+
const strategyRow = React.createElement(Text, {
|
|
252
|
+
width: rowWidth,
|
|
253
|
+
}, paddedStates.map((state, cellIndex) => React.createElement(Text, {
|
|
254
|
+
key: `strategy-cell-${cellIndex}`,
|
|
255
|
+
color: state ? toneByState[state] ?? colors.neutral : colors.inactive,
|
|
256
|
+
}, `${strengthCellGlyph(Boolean(state), colorEnabled)}${cellIndex < paddedStates.length - 1 ? " " : ""}`)));
|
|
257
|
+
const labels = segments.map((segment) => `${segment.key[0].toUpperCase()}${segment.key.slice(1)} ${segment.count}`);
|
|
258
|
+
|
|
259
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
260
|
+
strategyRow,
|
|
261
|
+
React.createElement(Box, { flexDirection: "row", width: rowWidth + 1, marginTop: 1 },
|
|
262
|
+
labels.map((label, index) => strategyLabel(
|
|
263
|
+
label,
|
|
264
|
+
segments[index].tone,
|
|
265
|
+
index === 0 ? "flex-start" : index === labels.length - 1 ? "flex-end" : "center",
|
|
266
|
+
segments.length,
|
|
267
|
+
)),
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function StatusLine({ icon, iconColor, textColor, children, marginTop = 0 }) {
|
|
273
|
+
return React.createElement(Box, { flexDirection: "row", marginTop },
|
|
274
|
+
React.createElement(Box, { width: 3 },
|
|
275
|
+
React.createElement(Text, { color: iconColor, bold: true }, icon),
|
|
276
|
+
),
|
|
277
|
+
React.createElement(Text, { color: textColor }, children),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function OutcomeTable({ outcomes, colorEnabled }) {
|
|
282
|
+
const colors = palette(colorEnabled);
|
|
283
|
+
const hasHistoricalOutcomes = outcomes.some((outcome) => [outcome.mean, outcome.winRate, outcome.liftBps]
|
|
284
|
+
.some((value) => typeof value === "number" && Number.isFinite(value)));
|
|
285
|
+
if (!hasHistoricalOutcomes) {
|
|
286
|
+
return React.createElement(Text, { color: colors.content }, "No historical outcomes available.");
|
|
287
|
+
}
|
|
288
|
+
const rows = [
|
|
289
|
+
[
|
|
290
|
+
tableValue("Timeframe", "muted", colorEnabled, { bold: true }),
|
|
291
|
+
...outcomes.map((outcome) => tableValue(`${outcome.horizon} Minutes`, "white", colorEnabled, { bold: true, hAlign: "right" })),
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
tableValue("Avg. Return", "muted", colorEnabled, { bold: true }),
|
|
295
|
+
...outcomes.map((outcome) => tableValue(numberLabel(outcome.mean), metricTone(outcome.mean), colorEnabled, { bold: true, hAlign: "right" })),
|
|
296
|
+
],
|
|
297
|
+
[
|
|
298
|
+
tableValue("Win Rate", "muted", colorEnabled, { bold: true }),
|
|
299
|
+
...outcomes.map((outcome) => tableValue(rateLabel(outcome.winRate), "white", colorEnabled, { bold: true, hAlign: "right" })),
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
tableValue("Lift (BPS)", "muted", colorEnabled, { bold: true }),
|
|
303
|
+
...outcomes.map((outcome) => tableValue(bpsLabel(outcome.liftBps), metricTone(outcome.liftBps), colorEnabled, { bold: true, hAlign: "right" })),
|
|
304
|
+
],
|
|
305
|
+
];
|
|
306
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
307
|
+
React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled)),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sideTone(side, colors) {
|
|
312
|
+
if (side === "buy") return colors.green;
|
|
313
|
+
if (side === "sell") return colors.red;
|
|
314
|
+
return colors.white;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function SectionHeading({ label, meta, colors, width = "100%" }) {
|
|
318
|
+
return React.createElement(Box, {
|
|
319
|
+
flexDirection: "row",
|
|
320
|
+
width,
|
|
321
|
+
justifyContent: "space-between",
|
|
322
|
+
},
|
|
323
|
+
React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
|
|
324
|
+
React.createElement(Box, { width: 2 },
|
|
325
|
+
React.createElement(Text, { color: colors.green, bold: true }, "▲"),
|
|
326
|
+
),
|
|
327
|
+
React.createElement(Text, { color: colors.white, bold: true }, label),
|
|
328
|
+
),
|
|
329
|
+
meta ? React.createElement(Text, { color: colors.white, bold: true }, meta) : null,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function SectionBody({ children, colors, paddingLeft = 1 }) {
|
|
334
|
+
return React.createElement(Box, {
|
|
335
|
+
flexDirection: "column",
|
|
336
|
+
width: "100%",
|
|
337
|
+
borderStyle: "single",
|
|
338
|
+
borderColor: colors.border,
|
|
339
|
+
borderTop: false,
|
|
340
|
+
borderBottom: false,
|
|
341
|
+
borderRight: false,
|
|
342
|
+
paddingLeft,
|
|
343
|
+
}, children);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function fallbackMarketSessionStatus(value = new Date()) {
|
|
347
|
+
const dateParts = new Intl.DateTimeFormat("en-US", {
|
|
348
|
+
timeZone: "America/New_York",
|
|
349
|
+
weekday: "short",
|
|
350
|
+
hour: "2-digit",
|
|
351
|
+
minute: "2-digit",
|
|
352
|
+
hourCycle: "h23",
|
|
353
|
+
}).formatToParts(value);
|
|
354
|
+
const weekday = dateParts.find((part) => part.type === "weekday")?.value;
|
|
355
|
+
const hour = Number(dateParts.find((part) => part.type === "hour")?.value ?? NaN);
|
|
356
|
+
const minute = Number(dateParts.find((part) => part.type === "minute")?.value ?? NaN);
|
|
357
|
+
const currentMinute = hour * 60 + minute;
|
|
358
|
+
return ["Mon", "Tue", "Wed", "Thu", "Fri"].includes(weekday)
|
|
359
|
+
&& Number.isFinite(currentMinute)
|
|
360
|
+
&& currentMinute >= 9 * 60 + 30
|
|
361
|
+
&& currentMinute < 16 * 60
|
|
362
|
+
? "open"
|
|
363
|
+
: "closed";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function RailSpacer({ colors }) {
|
|
367
|
+
return React.createElement(Box, {
|
|
368
|
+
height: 1,
|
|
369
|
+
width: "100%",
|
|
370
|
+
borderStyle: "single",
|
|
371
|
+
borderColor: colors.border,
|
|
372
|
+
borderTop: false,
|
|
373
|
+
borderBottom: false,
|
|
374
|
+
borderRight: false,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function QueryHeader({ view, colors }) {
|
|
379
|
+
const side = typeof view.side === "string" ? view.side.toUpperCase() : "MARKET";
|
|
380
|
+
const marketSessionStatus = view.marketSessionStatus ?? fallbackMarketSessionStatus();
|
|
381
|
+
const marketIsOpen = marketSessionStatus === "open";
|
|
382
|
+
const timestamp = [view.frameDate, view.frameTime]
|
|
383
|
+
.filter((value) => value && value !== "—")
|
|
384
|
+
.join(" · ") || "—";
|
|
385
|
+
return React.createElement(Box, {
|
|
386
|
+
flexDirection: "column",
|
|
387
|
+
width: "100%",
|
|
388
|
+
marginTop: 1,
|
|
389
|
+
},
|
|
390
|
+
React.createElement(Box, { flexDirection: "row", width: "100%", paddingRight: 2, justifyContent: "space-between" },
|
|
391
|
+
React.createElement(Text, { color: colors.white },
|
|
392
|
+
React.createElement(Text, { color: sideTone(view.side, colors), bold: true }, side),
|
|
393
|
+
" · ",
|
|
394
|
+
React.createElement(Text, { color: colors.white, bold: true }, view.symbol),
|
|
395
|
+
" @ ",
|
|
396
|
+
view.last,
|
|
397
|
+
),
|
|
398
|
+
React.createElement(Text, { color: colors.muted }, timestamp),
|
|
399
|
+
),
|
|
400
|
+
React.createElement(Box, { flexDirection: "row", width: "100%", paddingRight: 2, justifyContent: "flex-end" },
|
|
401
|
+
React.createElement(Text, { color: marketIsOpen ? colors.green : colors.red, bold: true }, "●"),
|
|
402
|
+
React.createElement(Text, { color: colors.white, bold: true }, ` Market ${marketIsOpen ? "Open" : "Closed"}`),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function HistoricalEvidence({ view, colorEnabled }) {
|
|
408
|
+
const sampleLabel = view.match.score === 5
|
|
409
|
+
? " exact observations"
|
|
410
|
+
: view.match.score > 0
|
|
411
|
+
? " comparable observations"
|
|
412
|
+
: " observations";
|
|
413
|
+
const matchLabel = view.match.label === "No match" ? "No Match" : `${view.match.label} Match`;
|
|
414
|
+
const matchDetails = view.match.score > 0
|
|
415
|
+
? `${view.match.rowCount}${sampleLabel}`
|
|
416
|
+
: "No comparable observations.";
|
|
417
|
+
const matchStrength = [
|
|
418
|
+
Array.from({ length: 5 }, (_, index) => tableContent(
|
|
419
|
+
strengthCellGlyph(index < view.match.score, colorEnabled),
|
|
420
|
+
index < view.match.score ? "green" : "neutral",
|
|
421
|
+
colorEnabled,
|
|
422
|
+
)).join(" "),
|
|
423
|
+
tableContent(matchLabel, view.match.score > 0 ? "white" : "muted", colorEnabled, true),
|
|
424
|
+
].join(" ");
|
|
425
|
+
const rows = [
|
|
426
|
+
[
|
|
427
|
+
tableValue("Match Strength", "muted", colorEnabled, { bold: true }),
|
|
428
|
+
{ content: matchStrength, hAlign: "left" },
|
|
429
|
+
],
|
|
430
|
+
[
|
|
431
|
+
tableValue("Match Details", "muted", colorEnabled, { bold: true }),
|
|
432
|
+
tableValue(matchDetails, "content", colorEnabled, { bold: true }),
|
|
433
|
+
],
|
|
434
|
+
[
|
|
435
|
+
tableValue("Date Range", "muted", colorEnabled, { bold: true }),
|
|
436
|
+
tableValue(view.archiveWindow, "content", colorEnabled),
|
|
437
|
+
],
|
|
438
|
+
];
|
|
439
|
+
return React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled, { divider: false }));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function LoadingSteps({ columns, colorEnabled, symbol, progress, error, paddingX = 2, terminalWidth }) {
|
|
443
|
+
const colors = palette(colorEnabled);
|
|
444
|
+
const width = terminalWidth ?? clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
|
|
445
|
+
const [spinnerIndex, setSpinnerIndex] = useState(0);
|
|
446
|
+
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
if (progress.completed >= 4) return undefined;
|
|
449
|
+
const timer = setInterval(() => {
|
|
450
|
+
setSpinnerIndex((current) => (current + 1) % SPINNER_FRAMES.length);
|
|
451
|
+
}, 90);
|
|
452
|
+
return () => clearInterval(timer);
|
|
453
|
+
}, [colorEnabled, progress.completed]);
|
|
454
|
+
|
|
455
|
+
const steps = [
|
|
456
|
+
`Loading live market data for ${String(symbol).toUpperCase()}.`,
|
|
457
|
+
`Running ${STRATEGY_COUNT} strategy evaluations.`,
|
|
458
|
+
"Checking for historical matches.",
|
|
459
|
+
progress.durationMs === null ? "Finishing…" : `Finished in ${progress.durationMs}ms.`,
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
return React.createElement(Box, {
|
|
463
|
+
flexDirection: "column",
|
|
464
|
+
width,
|
|
465
|
+
paddingX,
|
|
466
|
+
}, steps.map((label, index) => {
|
|
467
|
+
const isDone = index < progress.completed;
|
|
468
|
+
const isActive = index === progress.completed && progress.completed < steps.length;
|
|
469
|
+
const isFailed = Boolean(error) && isActive;
|
|
470
|
+
return React.createElement(StatusLine, {
|
|
471
|
+
key: label,
|
|
472
|
+
icon: isFailed ? "!" : isDone ? "✓" : isActive ? SPINNER_FRAMES[spinnerIndex] : "·",
|
|
473
|
+
iconColor: isFailed ? colors.red : isDone ? colors.green : isActive ? colors.white : colors.muted,
|
|
474
|
+
textColor: isDone ? colors.white : isFailed ? colors.white : colors.muted,
|
|
475
|
+
marginTop: index === 0 ? 1 : 0,
|
|
476
|
+
}, isFailed ? `${label} ${error}` : label);
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function EvidenceView({ response, colorEnabled, columns, phase, updatedAt, error, progress }) {
|
|
482
|
+
const colors = palette(colorEnabled);
|
|
483
|
+
const view = buildEvaluationViewModel(response);
|
|
484
|
+
const width = clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
|
|
485
|
+
const sectionWidth = Math.max(1, width - 6);
|
|
486
|
+
const updatedText = updatedAt
|
|
487
|
+
? new Date(updatedAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
|
|
488
|
+
: "—";
|
|
489
|
+
const liveTone = phase === "error" ? colors.red : phase === "checking" ? "yellow" : colors.green;
|
|
490
|
+
if (view.status === "artifact_unavailable") {
|
|
491
|
+
return React.createElement(Box, {
|
|
492
|
+
flexDirection: "column",
|
|
493
|
+
width,
|
|
494
|
+
paddingX: 2,
|
|
495
|
+
},
|
|
496
|
+
React.createElement(StatusLine, { icon: "!", iconColor: liveTone, textColor: colors.white, marginTop: 2 }, "Market Memory unavailable"),
|
|
497
|
+
React.createElement(Text, { color: colors.muted, marginTop: 1 }, "The private evidence reader is unavailable. Retrying on the next tape check."),
|
|
498
|
+
React.createElement(Text, { color: colors.muted, marginTop: 1 }, `updated ${updatedText} · q/esc to exit`),
|
|
499
|
+
error ? React.createElement(StatusLine, { icon: "!", iconColor: colors.red, textColor: colors.white, marginTop: 1 }, error) : null,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return React.createElement(Box, {
|
|
504
|
+
flexDirection: "column",
|
|
505
|
+
width,
|
|
506
|
+
paddingX: 2,
|
|
507
|
+
},
|
|
508
|
+
React.createElement(LoadingSteps, {
|
|
509
|
+
columns,
|
|
510
|
+
colorEnabled,
|
|
511
|
+
symbol: view.symbol,
|
|
512
|
+
progress,
|
|
513
|
+
error: null,
|
|
514
|
+
paddingX: 0,
|
|
515
|
+
terminalWidth: Math.max(1, width - 4),
|
|
516
|
+
}),
|
|
517
|
+
React.createElement(QueryHeader, { view, colors }),
|
|
518
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
519
|
+
React.createElement(SectionHeading, {
|
|
520
|
+
label: "Strategy Signals",
|
|
521
|
+
meta: `${view.strategy.total} Evaluated`,
|
|
522
|
+
colors,
|
|
523
|
+
width: sectionWidth,
|
|
524
|
+
}),
|
|
525
|
+
React.createElement(RailSpacer, { colors }),
|
|
526
|
+
React.createElement(SectionBody, { colors },
|
|
527
|
+
React.createElement(StrategyMix, {
|
|
528
|
+
strategy: view.strategy,
|
|
529
|
+
colorEnabled,
|
|
530
|
+
}),
|
|
531
|
+
),
|
|
532
|
+
),
|
|
533
|
+
React.createElement(RailSpacer, { colors }),
|
|
534
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
535
|
+
React.createElement(SectionHeading, { label: "Market Context", colors, width: sectionWidth }),
|
|
536
|
+
React.createElement(RailSpacer, { colors }),
|
|
537
|
+
React.createElement(SectionBody, { colors },
|
|
538
|
+
React.createElement(MarketSetupTable, {
|
|
539
|
+
symbol: view.symbol,
|
|
540
|
+
setup: view.marketSetup,
|
|
541
|
+
market: view.marketOverview,
|
|
542
|
+
colorEnabled,
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
React.createElement(RailSpacer, { colors }),
|
|
547
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
548
|
+
React.createElement(SectionHeading, { label: "Historical Evidence", colors, width: sectionWidth }),
|
|
549
|
+
React.createElement(RailSpacer, { colors }),
|
|
550
|
+
React.createElement(SectionBody, { colors },
|
|
551
|
+
React.createElement(HistoricalEvidence, { view, colorEnabled }),
|
|
552
|
+
),
|
|
553
|
+
),
|
|
554
|
+
React.createElement(RailSpacer, { colors }),
|
|
555
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
556
|
+
React.createElement(SectionHeading, { label: "Observed Outcomes", colors, width: sectionWidth }),
|
|
557
|
+
React.createElement(RailSpacer, { colors }),
|
|
558
|
+
React.createElement(SectionBody, { colors },
|
|
559
|
+
React.createElement(OutcomeTable, {
|
|
560
|
+
outcomes: view.outcomes,
|
|
561
|
+
colorEnabled,
|
|
562
|
+
}),
|
|
563
|
+
),
|
|
564
|
+
),
|
|
565
|
+
error ? React.createElement(StatusLine, { icon: "!", iconColor: colors.red, textColor: colors.white, marginTop: 1 }, error) : null,
|
|
566
|
+
React.createElement(Box, { height: 2 }),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function LiveEvidenceApp({ client, request, colorEnabled = true, intervalMs = 60_000, polling = true }) {
|
|
571
|
+
const { columns = 80 } = useWindowSize();
|
|
572
|
+
const { exit } = useApp();
|
|
573
|
+
const [state, setState] = useState({ response: null, phase: "connecting", updatedAt: null, frameKey: null, updateCount: 0, error: null, progress: { completed: 0, durationMs: null } });
|
|
574
|
+
|
|
575
|
+
useInput((input, key) => {
|
|
576
|
+
if (input.toLowerCase() === "q" || key.escape) exit();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
let active = true;
|
|
581
|
+
let busy = false;
|
|
582
|
+
const refresh = async () => {
|
|
583
|
+
if (!active || busy) return;
|
|
584
|
+
busy = true;
|
|
585
|
+
const startedAt = Date.now();
|
|
586
|
+
setState((current) => ({ ...current, phase: current.response ? "checking" : "connecting", error: null }));
|
|
587
|
+
try {
|
|
588
|
+
const response = await client.evaluate(request);
|
|
589
|
+
if (!active) return;
|
|
590
|
+
const view = buildEvaluationViewModel(response);
|
|
591
|
+
const nextKey = frameKey(response, view);
|
|
592
|
+
const durationMs = Date.now() - startedAt;
|
|
593
|
+
for (let completed = 1; completed <= 4; completed += 1) {
|
|
594
|
+
if (!active) return;
|
|
595
|
+
setState((current) => ({
|
|
596
|
+
...current,
|
|
597
|
+
progress: { completed, durationMs: completed === 4 ? durationMs : current.progress.durationMs },
|
|
598
|
+
}));
|
|
599
|
+
if (completed < 4) await new Promise((resolve) => setTimeout(resolve, LOADING_STEP_DELAY_MS));
|
|
600
|
+
}
|
|
601
|
+
if (!active) return;
|
|
602
|
+
await new Promise((resolve) => setTimeout(resolve, LOADING_STEP_DELAY_MS));
|
|
603
|
+
if (!active) return;
|
|
604
|
+
setState((current) => ({
|
|
605
|
+
response,
|
|
606
|
+
phase: "live",
|
|
607
|
+
updatedAt: current.frameKey === nextKey ? current.updatedAt : Date.now(),
|
|
608
|
+
frameKey: nextKey,
|
|
609
|
+
updateCount: current.frameKey === nextKey ? current.updateCount : current.updateCount + 1,
|
|
610
|
+
error: null,
|
|
611
|
+
progress: { completed: 4, durationMs },
|
|
612
|
+
}));
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (!active) return;
|
|
615
|
+
setState((current) => ({ ...current, phase: "error", updatedAt: Date.now(), error: error instanceof Error ? error.message : String(error) }));
|
|
616
|
+
} finally {
|
|
617
|
+
busy = false;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
void refresh();
|
|
621
|
+
const timer = polling ? setInterval(() => { void refresh(); }, intervalMs) : null;
|
|
622
|
+
return () => {
|
|
623
|
+
active = false;
|
|
624
|
+
if (timer) clearInterval(timer);
|
|
625
|
+
};
|
|
626
|
+
}, [client, intervalMs, polling, request]);
|
|
627
|
+
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
if (polling || !state.response) return undefined;
|
|
630
|
+
const timer = setTimeout(() => exit(), 120);
|
|
631
|
+
return () => clearTimeout(timer);
|
|
632
|
+
}, [exit, polling, state.response]);
|
|
633
|
+
|
|
634
|
+
if (!state.response) {
|
|
635
|
+
return React.createElement(LoadingSteps, {
|
|
636
|
+
columns,
|
|
637
|
+
colorEnabled,
|
|
638
|
+
symbol: request.symbol,
|
|
639
|
+
progress: state.progress,
|
|
640
|
+
error: state.error,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return React.createElement(EvidenceView, {
|
|
645
|
+
response: state.response,
|
|
646
|
+
colorEnabled,
|
|
647
|
+
columns,
|
|
648
|
+
phase: state.phase,
|
|
649
|
+
updatedAt: state.updatedAt,
|
|
650
|
+
error: state.error,
|
|
651
|
+
progress: state.progress,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export { render };
|