@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/format.mjs
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import { ansiForeground, LEROY_COLORS } from "./colors.mjs";
|
|
2
|
+
|
|
1
3
|
const ANSI = {
|
|
2
4
|
reset: "\u001b[0m",
|
|
3
5
|
bold: "\u001b[1m",
|
|
4
6
|
dim: "\u001b[2m",
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
white: "\u001b[37m",
|
|
8
|
+
green: ansiForeground(LEROY_COLORS.green),
|
|
9
|
+
gray: "\u001b[90m",
|
|
10
|
+
neutral: ansiForeground(LEROY_COLORS.neutral),
|
|
11
|
+
inactive: ansiForeground(LEROY_COLORS.inactive),
|
|
12
|
+
red: ansiForeground(LEROY_COLORS.red),
|
|
7
13
|
yellow: "\u001b[33m",
|
|
8
14
|
};
|
|
9
15
|
|
|
10
16
|
function color(value, tone, enabled) {
|
|
11
|
-
return
|
|
17
|
+
return style(value, [tone], enabled);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function style(value, tones, enabled) {
|
|
21
|
+
return enabled ? `${tones.map((tone) => ANSI[tone]).join("")}${value}${ANSI.reset}` : value;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
function record(value) {
|
|
@@ -23,7 +33,7 @@ function pct(value, digits = 2, enabled = true) {
|
|
|
23
33
|
const number = finite(value);
|
|
24
34
|
if (number === null) return "—";
|
|
25
35
|
const text = `${number > 0 ? "+" : number < 0 ? "−" : ""}${Math.abs(number).toFixed(digits)}%`;
|
|
26
|
-
return number >= 0 ? color(text, "green", enabled) : color(text, "
|
|
36
|
+
return number >= 0 ? color(text, "green", enabled) : color(text, "red", enabled);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
function dateLabel(value) {
|
|
@@ -62,17 +72,20 @@ function bestMemoryMatch(response) {
|
|
|
62
72
|
function strategyCounts(response, requestedSide) {
|
|
63
73
|
const counts = { buy: 0, neutral: 0, sell: 0, warming: 0 };
|
|
64
74
|
const states = Array.isArray(response.strategy_states) ? response.strategy_states : [];
|
|
75
|
+
const observedStates = [];
|
|
65
76
|
let observed = false;
|
|
66
77
|
for (const entry of states) {
|
|
67
78
|
const value = record(entry);
|
|
68
79
|
const state = value.state;
|
|
69
80
|
if (state === "warming" || state === "warming-up" || value.monitor_state === "warming-up") {
|
|
70
81
|
counts.warming += 1;
|
|
82
|
+
observedStates.push("warming");
|
|
71
83
|
observed = true;
|
|
72
84
|
continue;
|
|
73
85
|
}
|
|
74
86
|
if (state in counts) {
|
|
75
87
|
counts[state] += 1;
|
|
88
|
+
observedStates.push(state);
|
|
76
89
|
observed = true;
|
|
77
90
|
}
|
|
78
91
|
}
|
|
@@ -80,36 +93,43 @@ function strategyCounts(response, requestedSide) {
|
|
|
80
93
|
const activeCount = finite(record(response.active_confluence).active_signal_count);
|
|
81
94
|
if (activeCount !== null && (requestedSide === "buy" || requestedSide === "sell")) {
|
|
82
95
|
counts[requestedSide] = activeCount;
|
|
83
|
-
return { counts, observed: true };
|
|
96
|
+
return { counts, observed: true, states: Array.from({ length: activeCount }, () => requestedSide) };
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
return { counts, observed };
|
|
99
|
+
return { counts, observed, states: observedStates };
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
function memoryMatchCounts(response) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const countRows = (kinds) => matches
|
|
94
|
-
.filter((match) => kinds.includes(record(match).match_kind))
|
|
95
|
-
.reduce((total, match) => total + (finite(record(match).row_count) ?? 0), 0);
|
|
103
|
+
const selected = bestMemoryMatch(response);
|
|
104
|
+
const tier = matchTier(selected);
|
|
105
|
+
const rowCount = finite(record(selected).row_count) ?? 0;
|
|
96
106
|
return {
|
|
97
|
-
exact:
|
|
98
|
-
comparable:
|
|
99
|
-
|
|
107
|
+
exact: tier.score === 5 ? rowCount : 0,
|
|
108
|
+
comparable: tier.score > 0 && tier.score < 5 ? rowCount : 0,
|
|
109
|
+
tier,
|
|
100
110
|
};
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
function
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
function matchTier(match) {
|
|
114
|
+
const value = record(match);
|
|
115
|
+
const kind = value.match_kind;
|
|
116
|
+
const scope = typeof value.scope === "string" ? value.scope : null;
|
|
117
|
+
if (scope === "same_ticker_exact" || (kind === "context" && !scope)) {
|
|
118
|
+
return { score: 5, basis: "Exact ticker setup" };
|
|
119
|
+
}
|
|
120
|
+
if (scope === "cross_ticker_exact_state" || kind === "cross_ticker_context") {
|
|
121
|
+
return { score: 4, basis: "Same setup on other tickers" };
|
|
122
|
+
}
|
|
123
|
+
if (scope === "exact_41_sensor_state" || kind === "strategy") {
|
|
124
|
+
return { score: 3, basis: "Same 41-sensor state" };
|
|
125
|
+
}
|
|
126
|
+
if (scope?.startsWith("forward_overlay_") || kind === "forward_overlay_context") {
|
|
127
|
+
return { score: 2, basis: "Same side with strategy and market context" };
|
|
128
|
+
}
|
|
129
|
+
if (scope === "all_market_memory_observations" || kind === "side") {
|
|
130
|
+
return { score: 1, basis: "Same side baseline" };
|
|
131
|
+
}
|
|
132
|
+
return { score: 0, basis: null };
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
function memoryStatus(response) {
|
|
@@ -120,20 +140,9 @@ function memoryStatus(response) {
|
|
|
120
140
|
};
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
function evidenceScope(response) {
|
|
124
|
-
const match = record(bestMemoryMatch(response));
|
|
125
|
-
const scope = match.scope;
|
|
126
|
-
if (scope === "same_ticker_exact") return "exact ticker setup";
|
|
127
|
-
if (scope === "cross_ticker_exact_state") return "cross-ticker exact state";
|
|
128
|
-
if (scope === "exact_41_sensor_state") return "41-sensor state";
|
|
129
|
-
if (scope?.startsWith("forward_overlay_")) return "forward overlay context";
|
|
130
|
-
if (scope === "all_market_memory_observations") return "directional baseline";
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
143
|
function unavailableReason(response) {
|
|
135
144
|
const { status, reason } = memoryStatus(response);
|
|
136
|
-
if (reason) return `${status
|
|
145
|
+
if (reason) return `${humanizeStatus(status)} · ${reason}`;
|
|
137
146
|
if (status === "artifact_unavailable") return "reader unavailable";
|
|
138
147
|
if (status === "insufficient_context") return "not available in current evidence";
|
|
139
148
|
return "not available in current evidence";
|
|
@@ -157,17 +166,18 @@ function lookupMilliseconds(response) {
|
|
|
157
166
|
|
|
158
167
|
function humanizeStatus(value) {
|
|
159
168
|
const labels = {
|
|
160
|
-
historical_match: "Historical
|
|
161
|
-
insufficient_context: "Limited
|
|
162
|
-
artifact_unavailable: "Reader
|
|
169
|
+
historical_match: "Historical Match",
|
|
170
|
+
insufficient_context: "Limited Match",
|
|
171
|
+
artifact_unavailable: "Reader Unavailable",
|
|
163
172
|
};
|
|
164
173
|
if (typeof value !== "string" || !value) return "Unavailable";
|
|
165
174
|
return labels[value] ?? value.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
function memorySummary(status,
|
|
169
|
-
if (status === "historical_match" &&
|
|
170
|
-
|
|
177
|
+
function memorySummary(status, tier) {
|
|
178
|
+
if (status === "historical_match" && tier.score === 5) return "Match Found";
|
|
179
|
+
if (tier.score > 0) return "Limited Match";
|
|
180
|
+
return humanizeStatus(status);
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
function sideLabel(value) {
|
|
@@ -175,17 +185,38 @@ function sideLabel(value) {
|
|
|
175
185
|
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
|
|
188
|
+
const BOX_WIDTH = 65;
|
|
189
|
+
|
|
190
|
+
function sideTone(value) {
|
|
191
|
+
if (value === "buy") return "green";
|
|
192
|
+
if (value === "sell") return "red";
|
|
193
|
+
if (value === "both") return "yellow";
|
|
194
|
+
return "dim";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sectionRule(title, enabled, width = BOX_WIDTH) {
|
|
179
198
|
const dashCount = Math.max(4, width - title.length - 5);
|
|
180
199
|
return color(`┌─ ${title} ${"─".repeat(dashCount)}┐`, "dim", enabled);
|
|
181
200
|
}
|
|
182
201
|
|
|
183
|
-
function
|
|
202
|
+
function tickerRule(side, symbol, last, enabled) {
|
|
203
|
+
const sideText = sideLabel(side).toUpperCase();
|
|
204
|
+
const title = `${sideText} · ${symbol} @ ${last}`;
|
|
205
|
+
const dashCount = Math.max(4, BOX_WIDTH - title.length - 5);
|
|
206
|
+
return [
|
|
207
|
+
color("┌─ ", "dim", enabled),
|
|
208
|
+
style(sideText, ["bold", sideTone(side)], enabled),
|
|
209
|
+
style(` · ${symbol} @ ${last}`, ["bold", "white"], enabled),
|
|
210
|
+
color(` ${"─".repeat(dashCount)}┐`, "dim", enabled),
|
|
211
|
+
].join("");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sectionBottom(enabled, width = BOX_WIDTH) {
|
|
184
215
|
return color(`└${"─".repeat(width - 2)}┘`, "dim", enabled);
|
|
185
216
|
}
|
|
186
217
|
|
|
187
|
-
function metricLine(label, value, tone, enabled) {
|
|
188
|
-
return ` ${label.padEnd(
|
|
218
|
+
function metricLine(label, value, tone, enabled, labelWidth = 26) {
|
|
219
|
+
return ` ${label.padEnd(labelWidth)}${color(value, tone, enabled)}`;
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
function normalizedRate(value) {
|
|
@@ -201,7 +232,7 @@ function rateLabel(value) {
|
|
|
201
232
|
|
|
202
233
|
function rateTone(value) {
|
|
203
234
|
const rate = normalizedRate(value);
|
|
204
|
-
return rate === null ? "dim" : rate >= 0.5 ? "green" : "
|
|
235
|
+
return rate === null ? "dim" : rate >= 0.5 ? "green" : "red";
|
|
205
236
|
}
|
|
206
237
|
|
|
207
238
|
function bps(value) {
|
|
@@ -212,7 +243,7 @@ function bps(value) {
|
|
|
212
243
|
|
|
213
244
|
function outcomeTone(value) {
|
|
214
245
|
const number = finite(value);
|
|
215
|
-
return number === null || number === 0 ? "dim" : number > 0 ? "green" : "
|
|
246
|
+
return number === null || number === 0 ? "dim" : number > 0 ? "green" : "red";
|
|
216
247
|
}
|
|
217
248
|
|
|
218
249
|
function coverageWindow(response, match) {
|
|
@@ -275,28 +306,292 @@ function ratioLabel(value) {
|
|
|
275
306
|
return number === null ? null : `${number.toFixed(2)}x`;
|
|
276
307
|
}
|
|
277
308
|
|
|
309
|
+
function horizonLabel(horizon) {
|
|
310
|
+
return `${horizon} Minutes`;
|
|
311
|
+
}
|
|
312
|
+
|
|
278
313
|
function setupLine(label, value, tone, enabled) {
|
|
279
|
-
const innerWidth =
|
|
314
|
+
const innerWidth = BOX_WIDTH - 4;
|
|
280
315
|
const labelWidth = 21;
|
|
281
316
|
const valueText = String(value ?? "—").slice(0, innerWidth - labelWidth);
|
|
282
317
|
return ` ${label.padEnd(labelWidth)}${color(valueText, tone, enabled)}`;
|
|
283
318
|
}
|
|
284
319
|
|
|
285
|
-
function outcomeRow(label, values, enabled
|
|
320
|
+
function outcomeRow(label, values, enabled) {
|
|
286
321
|
const labelWidth = 19;
|
|
287
|
-
const cellWidth =
|
|
322
|
+
const cellWidth = 14;
|
|
288
323
|
const cells = values.map(({ value, tone }) => {
|
|
289
324
|
const text = String(value);
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
const left = align === "center" ? Math.floor(padding / 2) : align === "left" ? 0 : padding;
|
|
293
|
-
const right = padding - left + 1;
|
|
294
|
-
return `│${" ".repeat(left)}${color(text, tone, enabled)}${" ".repeat(right)}`;
|
|
325
|
+
const padding = Math.max(0, cellWidth - text.length);
|
|
326
|
+
return `${" ".repeat(padding)}${color(text, tone, enabled)}`;
|
|
295
327
|
}).join("");
|
|
296
|
-
return ` ${label.padEnd(labelWidth)}${cells}
|
|
328
|
+
return ` ${label.padEnd(labelWidth)}${cells} `;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function compactMetricLine(label, value, tone, enabled) {
|
|
332
|
+
return ` ${label}: ${color(String(value ?? "—"), tone, enabled)}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function compactOutcomeLine(row, enabled) {
|
|
336
|
+
const returnValue = row.mean === null ? "—" : pct(row.mean, 2, false);
|
|
337
|
+
const winValue = rateLabel(row.winRate);
|
|
338
|
+
const liftValue = bps(row.liftBps).replace(/ bps$/, "");
|
|
339
|
+
return ` ${horizonLabel(row.horizon)} ${color(`${returnValue} return · ${winValue} win · ${liftValue} bps`, outcomeTone(row.mean), enabled)}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const STRATEGY_MIX_SLOTS = 20;
|
|
343
|
+
|
|
344
|
+
function strategyMixWidths(counts, total, slots = STRATEGY_MIX_SLOTS) {
|
|
345
|
+
if (total <= 0) return [0, 0, 0, 0];
|
|
346
|
+
const segments = [
|
|
347
|
+
counts.buy,
|
|
348
|
+
counts.neutral,
|
|
349
|
+
counts.sell,
|
|
350
|
+
counts.warming,
|
|
351
|
+
];
|
|
352
|
+
const widths = segments.map((count) => count > 0 ? Math.max(1, Math.round((count / total) * slots)) : 0);
|
|
353
|
+
let occupied = widths.reduce((sum, value) => sum + value, 0);
|
|
354
|
+
while (occupied > slots) {
|
|
355
|
+
const index = widths.reduce((best, value, position) => {
|
|
356
|
+
if (value <= 1) return best;
|
|
357
|
+
return best === -1 || value > widths[best] || (value === widths[best] && segments[position] > segments[best]) ? position : best;
|
|
358
|
+
}, -1);
|
|
359
|
+
if (index < 0) break;
|
|
360
|
+
widths[index] -= 1;
|
|
361
|
+
occupied -= 1;
|
|
362
|
+
}
|
|
363
|
+
while (occupied < slots) {
|
|
364
|
+
const index = segments.reduce((best, count, position) => count > segments[best] ? position : best, 0);
|
|
365
|
+
widths[index] += 1;
|
|
366
|
+
occupied += 1;
|
|
367
|
+
}
|
|
368
|
+
return widths;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function strategyMixLines(counts, total, enabled) {
|
|
372
|
+
if (total <= 0) return [color("—", "dim", enabled)];
|
|
373
|
+
const widths = strategyMixWidths(counts, total);
|
|
374
|
+
const slots = [
|
|
375
|
+
{ count: counts.buy, label: `Buy ${countLabel(counts.buy)}`, tone: "green" },
|
|
376
|
+
{ count: counts.neutral, label: `Neutral ${countLabel(counts.neutral)}`, tone: "neutral" },
|
|
377
|
+
{ count: counts.sell, label: `Sell ${countLabel(counts.sell)}`, tone: "red" },
|
|
378
|
+
{ count: counts.warming, label: `Warming ${countLabel(counts.warming)}`, tone: "yellow" },
|
|
379
|
+
];
|
|
380
|
+
const cells = slots.flatMap((segment, index) => Array.from({ length: widths[index] }, () => color("■", segment.tone, enabled)));
|
|
381
|
+
const labelWidth = slots.length === 4 && counts.warming > 0 ? 52 : 39;
|
|
382
|
+
const columnWidth = labelWidth / (counts.warming > 0 ? 4 : 3);
|
|
383
|
+
const visibleSlots = counts.warming > 0 ? slots : slots.slice(0, 3);
|
|
384
|
+
const buyLabel = visibleSlots[0].label.padEnd(columnWidth);
|
|
385
|
+
const neutralLabel = visibleSlots[1].label.padStart(Math.floor((columnWidth + visibleSlots[1].label.length) / 2)).padEnd(columnWidth);
|
|
386
|
+
const sellLabel = visibleSlots[2].label.padStart(columnWidth);
|
|
387
|
+
const labels = [buyLabel, neutralLabel, sellLabel];
|
|
388
|
+
if (counts.warming > 0) labels.push(visibleSlots[3].label.padStart(columnWidth));
|
|
389
|
+
return [cells.join(" "), labels.map((label, index) => color(label, visibleSlots[index].tone, enabled)).join("")];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const GRID_LEFT_WIDTH = 32;
|
|
393
|
+
const GRID_TOTAL_WIDTH = 58;
|
|
394
|
+
|
|
395
|
+
function gridColumns(left, right, colorEnabled) {
|
|
396
|
+
const leftText = left?.text ?? "";
|
|
397
|
+
const rightText = right?.text ?? "";
|
|
398
|
+
const leftCell = leftText
|
|
399
|
+
? color(leftText.padEnd(GRID_LEFT_WIDTH), left.tone ?? "white", colorEnabled)
|
|
400
|
+
: " ".repeat(GRID_LEFT_WIDTH);
|
|
401
|
+
const rightCell = rightText
|
|
402
|
+
? right.tone
|
|
403
|
+
? color(rightText, right.tone, colorEnabled)
|
|
404
|
+
: rightText
|
|
405
|
+
: "";
|
|
406
|
+
return `${leftCell}${rightCell}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function gridFrameParts(value) {
|
|
410
|
+
if (typeof value !== "string") return { date: "—", time: "" };
|
|
411
|
+
const date = new Date(value);
|
|
412
|
+
if (Number.isNaN(date.valueOf())) return { date: value, time: "" };
|
|
413
|
+
return {
|
|
414
|
+
date: new Intl.DateTimeFormat("en-US", {
|
|
415
|
+
month: "short",
|
|
416
|
+
day: "numeric",
|
|
417
|
+
year: "numeric",
|
|
418
|
+
timeZone: "America/New_York",
|
|
419
|
+
}).format(date),
|
|
420
|
+
time: `${new Intl.DateTimeFormat("en-US", {
|
|
421
|
+
hour: "numeric",
|
|
422
|
+
minute: "2-digit",
|
|
423
|
+
timeZone: "America/New_York",
|
|
424
|
+
}).format(date)} ET`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function gridMatchColumns(match, colorEnabled) {
|
|
429
|
+
const tier = matchTier(match);
|
|
430
|
+
const label = tier.score === 5 ? "Exact" : tier.score > 0 ? "Limited" : "No match";
|
|
431
|
+
const rowCount = countLabel(record(match).row_count);
|
|
432
|
+
const countLabelText = tier.score === 5 ? "exact observations" : "comparable observations";
|
|
433
|
+
const cells = Array.from({ length: 5 }, (_, index) => color(
|
|
434
|
+
index < tier.score ? "■" : "□",
|
|
435
|
+
index < tier.score ? "green" : "neutral",
|
|
436
|
+
colorEnabled,
|
|
437
|
+
)).join("");
|
|
438
|
+
return {
|
|
439
|
+
label: `${cells} ${label}`,
|
|
440
|
+
comparable: `${rowCount} ${countLabelText}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function gridHeading(label, value, enabled) {
|
|
445
|
+
const gap = Math.max(1, GRID_TOTAL_WIDTH - label.length - value.length);
|
|
446
|
+
return color(`${label}${" ".repeat(gap)}${value}`, "white", enabled);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function gridOutcomeLines(outcomeRows, colorEnabled) {
|
|
450
|
+
const cellWidth = 12;
|
|
451
|
+
const row = (label, values, tones) => [
|
|
452
|
+
` ${label.padEnd(16)}`,
|
|
453
|
+
...values.map((value, index) => color(String(value).padStart(cellWidth), tones[index], colorEnabled)),
|
|
454
|
+
].join(" ");
|
|
455
|
+
return [
|
|
456
|
+
color("OUTCOME LADDER", "white", colorEnabled),
|
|
457
|
+
row("Timeframe", outcomeRows.map((outcome) => horizonLabel(outcome.horizon)), ["white", "white", "white"]),
|
|
458
|
+
row("Avg. Return", outcomeRows.map((outcome) => outcome.mean === null ? "—" : pct(outcome.mean, 2, false)), outcomeRows.map((outcome) => outcomeTone(outcome.mean))),
|
|
459
|
+
row("Win Rate", outcomeRows.map((outcome) => rateLabel(outcome.winRate)), outcomeRows.map((outcome) => rateTone(outcome.winRate))),
|
|
460
|
+
row("Lift (BPS)", outcomeRows.map((outcome) => outcome.liftBps === null ? "—" : bps(outcome.liftBps).replace(/ bps$/, "")), outcomeRows.map((outcome) => outcomeTone(outcome.liftBps))),
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function buildEvaluationViewModel(response) {
|
|
465
|
+
const request = record(response.request);
|
|
466
|
+
const frame = record(response.current_market_frame);
|
|
467
|
+
const symbol = typeof request.symbol === "string" ? request.symbol : typeof frame.symbol === "string" ? frame.symbol : "—";
|
|
468
|
+
const requestedSide = typeof request.side === "string" ? request.side : null;
|
|
469
|
+
const strategy = strategyCounts(response, requestedSide);
|
|
470
|
+
const strategyTotal = Object.values(strategy.counts).reduce((total, value) => total + value, 0);
|
|
471
|
+
const match = record(bestMemoryMatch(response));
|
|
472
|
+
const tier = matchTier(match);
|
|
473
|
+
const frameParts = gridFrameParts(frame.source_time);
|
|
474
|
+
const outcomes = [15, 30, 60].map((horizon) => {
|
|
475
|
+
const outcome = record(outcomeFor(response, horizon));
|
|
476
|
+
return {
|
|
477
|
+
horizon,
|
|
478
|
+
mean: finite(outcome.mean_return_pct ?? outcome.average_return_pct),
|
|
479
|
+
winRate: finite(outcome.win_rate),
|
|
480
|
+
liftBps: finite(outcome.lift_vs_baseline_bps),
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
const frameContext = record(frame.market_context);
|
|
484
|
+
const benchmark = record(frame.benchmark);
|
|
485
|
+
const benchmarkContext = record(benchmark.market_context);
|
|
486
|
+
const setupContext = activeContext(response, requestedSide);
|
|
487
|
+
const marketSources = [frameContext, setupContext, frame];
|
|
488
|
+
const benchmarkSources = [benchmarkContext, benchmark];
|
|
489
|
+
const marketSetup = {
|
|
490
|
+
sessionReturnPct: finite(firstValue(marketSources, ["session_return_pct", "sessionReturnPct"])),
|
|
491
|
+
volumeRatio: finite(firstValue(marketSources, ["volume_ratio", "volumeRatio"])),
|
|
492
|
+
vwapDistancePct: finite(firstValue(marketSources, ["vwap_distance_pct", "vwapDistancePct"])),
|
|
493
|
+
atrPct: finite(firstValue(marketSources, ["atr_pct", "atrPct"])),
|
|
494
|
+
};
|
|
495
|
+
const marketOverview = {
|
|
496
|
+
symbol: typeof benchmark.symbol === "string" ? benchmark.symbol : "SPY",
|
|
497
|
+
sessionReturnPct: finite(firstValue(benchmarkSources, ["session_return_pct", "sessionReturnPct"])),
|
|
498
|
+
volumeRatio: finite(firstValue(benchmarkSources, ["volume_ratio", "volumeRatio"])),
|
|
499
|
+
vwapDistancePct: finite(firstValue(benchmarkSources, ["vwap_distance_pct", "vwapDistancePct"])),
|
|
500
|
+
atrPct: finite(firstValue(benchmarkSources, ["atr_pct", "atrPct"])),
|
|
501
|
+
};
|
|
502
|
+
const memory = memoryStatus(response);
|
|
503
|
+
return {
|
|
504
|
+
side: requestedSide,
|
|
505
|
+
symbol,
|
|
506
|
+
last: frame.quote?.last == null ? "—" : `$${Number(frame.quote.last).toFixed(2)}`,
|
|
507
|
+
frameDate: frameParts.date,
|
|
508
|
+
frameTime: frameParts.time,
|
|
509
|
+
frameSourceTime: typeof frame.source_time === "string" ? frame.source_time : null,
|
|
510
|
+
marketSessionStatus: frame.market_session_status === "open" || frame.market_session_status === "closed"
|
|
511
|
+
? frame.market_session_status
|
|
512
|
+
: null,
|
|
513
|
+
status: memory.status,
|
|
514
|
+
statusReason: memory.reason,
|
|
515
|
+
match: {
|
|
516
|
+
score: tier.score,
|
|
517
|
+
label: tier.score === 5 ? "Exact" : tier.score > 0 ? "Limited" : "No match",
|
|
518
|
+
rowCount: countLabel(record(match).row_count),
|
|
519
|
+
basis: tier.basis,
|
|
520
|
+
},
|
|
521
|
+
strategy: {
|
|
522
|
+
counts: strategy.counts,
|
|
523
|
+
total: strategyTotal,
|
|
524
|
+
widths: strategyMixWidths(strategy.counts, strategyTotal),
|
|
525
|
+
states: strategy.states,
|
|
526
|
+
},
|
|
527
|
+
outcomes,
|
|
528
|
+
marketSetup,
|
|
529
|
+
marketOverview,
|
|
530
|
+
archiveWindow: coverageWindow(response, match),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function formatGridEvaluation(response, { colorEnabled = true } = {}) {
|
|
535
|
+
const request = record(response.request);
|
|
536
|
+
const frame = record(response.current_market_frame);
|
|
537
|
+
const symbol = typeof request.symbol === "string" ? request.symbol : typeof frame.symbol === "string" ? frame.symbol : "—";
|
|
538
|
+
const requestedSide = typeof request.side === "string" ? request.side : null;
|
|
539
|
+
const counts = strategyCounts(response, requestedSide).counts;
|
|
540
|
+
const strategyTotal = Object.values(counts).reduce((total, value) => total + value, 0);
|
|
541
|
+
const match = record(bestMemoryMatch(response));
|
|
542
|
+
const last = frame.quote?.last == null ? "—" : `$${Number(frame.quote.last).toFixed(2)}`;
|
|
543
|
+
const outcomes = [15, 30, 60].map((horizon) => {
|
|
544
|
+
const outcome = record(outcomeFor(response, horizon));
|
|
545
|
+
return {
|
|
546
|
+
horizon,
|
|
547
|
+
mean: finite(outcome.mean_return_pct ?? outcome.average_return_pct),
|
|
548
|
+
winRate: finite(outcome.win_rate),
|
|
549
|
+
liftBps: finite(outcome.lift_vs_baseline_bps),
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
const frameParts = gridFrameParts(frame.source_time);
|
|
553
|
+
const sideText = sideLabel(requestedSide).toUpperCase();
|
|
554
|
+
const matchColumns = gridMatchColumns(match, colorEnabled);
|
|
555
|
+
const topLines = [
|
|
556
|
+
gridColumns(
|
|
557
|
+
{ text: `${sideText} EVIDENCE`, tone: sideTone(requestedSide) },
|
|
558
|
+
{ text: "MATCH STRENGTH", tone: "white" },
|
|
559
|
+
colorEnabled,
|
|
560
|
+
),
|
|
561
|
+
gridColumns(
|
|
562
|
+
{ text: `${symbol} @ ${last}`, tone: "bold" },
|
|
563
|
+
{ text: matchColumns.label, tone: "white" },
|
|
564
|
+
colorEnabled,
|
|
565
|
+
),
|
|
566
|
+
gridColumns(
|
|
567
|
+
{ text: `${frameParts.date} ${frameParts.time}`.trim(), tone: "gray" },
|
|
568
|
+
{ text: matchColumns.comparable, tone: "gray" },
|
|
569
|
+
colorEnabled,
|
|
570
|
+
),
|
|
571
|
+
];
|
|
572
|
+
const strategyLines = [
|
|
573
|
+
gridHeading("STRATEGY MIX", `${countLabel(strategyTotal)} EVALUATED`, colorEnabled),
|
|
574
|
+
...strategyMixLines(counts, strategyTotal, colorEnabled),
|
|
575
|
+
];
|
|
576
|
+
return [
|
|
577
|
+
...topLines,
|
|
578
|
+
"",
|
|
579
|
+
...strategyLines,
|
|
580
|
+
"",
|
|
581
|
+
...gridOutcomeLines(outcomes, colorEnabled),
|
|
582
|
+
color(` Archive Window ${coverageWindow(response, match)}`, "gray", colorEnabled),
|
|
583
|
+
"",
|
|
584
|
+
color("Leroy returns evidence; it does not make or submit trades.", "dim", colorEnabled),
|
|
585
|
+
].join("\n");
|
|
297
586
|
}
|
|
298
587
|
|
|
299
|
-
export function formatEvaluation(response, { colorEnabled = true } = {}) {
|
|
588
|
+
export function formatEvaluation(response, { colorEnabled = true, setup = false, verbose = false, compact = false, layout = "ledger" } = {}) {
|
|
589
|
+
if (layout === "grid") {
|
|
590
|
+
const { status } = memoryStatus(response);
|
|
591
|
+
return status === "artifact_unavailable"
|
|
592
|
+
? color("Market Memory is temporarily unavailable. Try again shortly.", "dim", colorEnabled)
|
|
593
|
+
: formatGridEvaluation(response, { colorEnabled });
|
|
594
|
+
}
|
|
300
595
|
const request = record(response.request);
|
|
301
596
|
const frame = record(response.current_market_frame);
|
|
302
597
|
const symbol = typeof request.symbol === "string" ? request.symbol : typeof frame.symbol === "string" ? frame.symbol : "—";
|
|
@@ -305,13 +600,14 @@ export function formatEvaluation(response, { colorEnabled = true } = {}) {
|
|
|
305
600
|
const strategyTotal = Object.values(counts.counts).reduce((total, value) => total + value, 0);
|
|
306
601
|
const match = record(bestMemoryMatch(response));
|
|
307
602
|
const matchCounts = memoryMatchCounts(response);
|
|
308
|
-
const evaluations = historicalEvaluations(response);
|
|
309
603
|
const { status } = memoryStatus(response);
|
|
310
|
-
|
|
604
|
+
if (status === "artifact_unavailable") {
|
|
605
|
+
return color("Market Memory is temporarily unavailable. Try again shortly.", "dim", colorEnabled);
|
|
606
|
+
}
|
|
311
607
|
const resultHorizons = [15, 30, 60];
|
|
312
608
|
const last = frame.quote?.last == null ? "—" : `$${Number(frame.quote.last).toFixed(2)}`;
|
|
313
609
|
const timestamp = frameLabel(frame.source_time);
|
|
314
|
-
const statusTone = status === "historical_match" ? "green" : status === "artifact_unavailable" ? "
|
|
610
|
+
const statusTone = status === "historical_match" ? "green" : status === "artifact_unavailable" ? "red" : "dim";
|
|
315
611
|
const outcomeRows = resultHorizons.map((resultHorizon) => {
|
|
316
612
|
const outcome = record(outcomeFor(response, resultHorizon));
|
|
317
613
|
const available = finite(outcome.sample_size) !== null && finite(outcome.sample_size) > 0;
|
|
@@ -326,6 +622,7 @@ export function formatEvaluation(response, { colorEnabled = true } = {}) {
|
|
|
326
622
|
const marketContext = record(frame.market_context);
|
|
327
623
|
const setupContext = activeContext(response, requestedSide);
|
|
328
624
|
const marketSources = [marketContext, setupContext, frame];
|
|
625
|
+
const lookupMs = lookupMilliseconds(response);
|
|
329
626
|
const marketRows = [
|
|
330
627
|
{
|
|
331
628
|
label: "Benchmark",
|
|
@@ -365,41 +662,76 @@ export function formatEvaluation(response, { colorEnabled = true } = {}) {
|
|
|
365
662
|
},
|
|
366
663
|
].filter((row) => row.raw !== null && row.display !== null);
|
|
367
664
|
const tickerSetup = [
|
|
368
|
-
|
|
369
|
-
setupLine("Price", last, "bold", colorEnabled),
|
|
665
|
+
tickerRule(requestedSide, symbol, last, colorEnabled),
|
|
370
666
|
setupLine("Frame", timestamp, "dim", colorEnabled),
|
|
371
|
-
setupLine("
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
667
|
+
setupLine("Lookup Time", lookupMs === null ? "—" : `${Math.round(lookupMs)}ms`, "dim", colorEnabled),
|
|
668
|
+
sectionBottom(colorEnabled),
|
|
669
|
+
];
|
|
670
|
+
const strategyScorecard = [
|
|
671
|
+
sectionRule(`Strategy Mix · ${countLabel(strategyTotal)} Evaluated`, colorEnabled),
|
|
672
|
+
...strategyMixLines(counts.counts, strategyTotal, colorEnabled),
|
|
377
673
|
sectionBottom(colorEnabled),
|
|
378
674
|
];
|
|
379
675
|
const outcomeGrid = [
|
|
380
|
-
sectionRule("Outcome
|
|
381
|
-
outcomeRow("
|
|
676
|
+
sectionRule("Outcome", colorEnabled),
|
|
677
|
+
outcomeRow("Timeframe", outcomeRows.map((row) => ({ value: horizonLabel(row.horizon), tone: "white" })), colorEnabled),
|
|
382
678
|
outcomeRow("Avg. Return", outcomeRows.map((row) => ({ value: row.mean === null ? "—" : pct(row.mean, 2, false), tone: outcomeTone(row.mean) })), colorEnabled),
|
|
383
679
|
outcomeRow("Win Rate", outcomeRows.map((row) => ({ value: rateLabel(row.winRate), tone: rateTone(row.winRate) })), colorEnabled),
|
|
384
|
-
outcomeRow("Lift
|
|
385
|
-
sectionBottom(colorEnabled
|
|
680
|
+
outcomeRow("Lift (bps)", outcomeRows.map((row) => ({ value: bps(row.liftBps).replace(/ bps$/, ""), tone: outcomeTone(row.liftBps) })), colorEnabled),
|
|
681
|
+
sectionBottom(colorEnabled),
|
|
386
682
|
];
|
|
387
683
|
const unavailableLines = outcomeRows
|
|
388
684
|
.filter((row) => !row.available)
|
|
389
|
-
.map((row) => ` ${row.horizon}
|
|
390
|
-
|
|
391
|
-
|
|
685
|
+
.map((row) => ` ${horizonLabel(row.horizon)} unavailable · ${unavailableReason(response)}`);
|
|
686
|
+
|
|
687
|
+
if (compact) {
|
|
688
|
+
const sideText = sideLabel(requestedSide).toUpperCase();
|
|
689
|
+
const compactLines = [
|
|
690
|
+
[
|
|
691
|
+
color(sideText, ["bold", sideTone(requestedSide)], colorEnabled),
|
|
692
|
+
style(` · ${symbol} @ ${last}`, ["bold", "white"], colorEnabled),
|
|
693
|
+
].join(""),
|
|
694
|
+
compactMetricLine("Frame", timestamp, "dim", colorEnabled),
|
|
695
|
+
compactMetricLine("Lookup Time", lookupMs === null ? "—" : `${Math.round(lookupMs)}ms`, "dim", colorEnabled),
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
if (verbose) {
|
|
699
|
+
compactLines.push("", `Strategy Mix · ${countLabel(strategyTotal)} Evaluated`, ...strategyMixLines(counts.counts, strategyTotal, colorEnabled));
|
|
700
|
+
}
|
|
701
|
+
if (setup && marketRows.length) {
|
|
702
|
+
compactLines.push("", "Market Setup", ...marketRows.map((row) => compactMetricLine(row.label, row.display, row.tone, colorEnabled)));
|
|
703
|
+
}
|
|
704
|
+
compactLines.push(
|
|
705
|
+
"",
|
|
706
|
+
"Market Memory",
|
|
707
|
+
compactMetricLine("Status", memorySummary(status, matchCounts.tier), statusTone, colorEnabled),
|
|
708
|
+
compactMetricLine("Match Strength", `${matchCounts.tier.score}/5`, matchCounts.tier.score === 5 ? "green" : matchCounts.tier.score > 0 ? "yellow" : "dim", colorEnabled),
|
|
709
|
+
...(matchCounts.tier.score === 5 ? [compactMetricLine("Exact Matches", countLabel(matchCounts.exact), "bold", colorEnabled)] : []),
|
|
710
|
+
...(matchCounts.tier.score > 0 && matchCounts.tier.score < 5 ? [compactMetricLine("Comparable Matches", countLabel(matchCounts.comparable), "bold", colorEnabled)] : []),
|
|
711
|
+
...(verbose && matchCounts.tier.basis ? [compactMetricLine("Match Basis", matchCounts.tier.basis, "dim", colorEnabled)] : []),
|
|
712
|
+
compactMetricLine("Window", coverageWindow(response, match), "dim", colorEnabled),
|
|
713
|
+
"",
|
|
714
|
+
"Outcome",
|
|
715
|
+
...outcomeRows.map((row) => compactOutcomeLine(row, colorEnabled)),
|
|
716
|
+
...unavailableLines,
|
|
717
|
+
"",
|
|
718
|
+
color("Leroy returns evidence; it does not make or submit trades.", "dim", colorEnabled),
|
|
719
|
+
);
|
|
720
|
+
return compactLines.join("\n");
|
|
721
|
+
}
|
|
722
|
+
|
|
392
723
|
const lines = [
|
|
393
724
|
...tickerSetup,
|
|
394
|
-
...(
|
|
725
|
+
...(verbose ? ["", ...strategyScorecard] : []),
|
|
726
|
+
...(setup && marketRows.length ? ["", sectionRule("Market Setup", colorEnabled), ...marketRows.map((row) => setupLine(row.label, row.display, row.tone, colorEnabled)), sectionBottom(colorEnabled)] : []),
|
|
395
727
|
"",
|
|
396
728
|
sectionRule("Market Memory", colorEnabled),
|
|
397
|
-
metricLine("Status", memorySummary(status,
|
|
398
|
-
metricLine("
|
|
399
|
-
metricLine("
|
|
400
|
-
metricLine("
|
|
401
|
-
metricLine("
|
|
402
|
-
metricLine("Window", coverageWindow(response, match), "dim", colorEnabled),
|
|
729
|
+
metricLine("Status", memorySummary(status, matchCounts.tier), statusTone, colorEnabled, 24),
|
|
730
|
+
metricLine("Match Strength", `${matchCounts.tier.score}/5`, matchCounts.tier.score === 5 ? "green" : matchCounts.tier.score > 0 ? "yellow" : "dim", colorEnabled, 24),
|
|
731
|
+
...(matchCounts.tier.score === 5 ? [metricLine("Exact Matches", countLabel(matchCounts.exact), "bold", colorEnabled, 24)] : []),
|
|
732
|
+
...(matchCounts.tier.score > 0 && matchCounts.tier.score < 5 ? [metricLine("Comparable Matches", countLabel(matchCounts.comparable), "bold", colorEnabled, 24)] : []),
|
|
733
|
+
...(verbose && matchCounts.tier.basis ? [metricLine("Match Basis", matchCounts.tier.basis, "dim", colorEnabled, 24)] : []),
|
|
734
|
+
metricLine("Window", coverageWindow(response, match), "dim", colorEnabled, 24),
|
|
403
735
|
sectionBottom(colorEnabled),
|
|
404
736
|
"",
|
|
405
737
|
...outcomeGrid,
|
|
@@ -415,11 +747,18 @@ export function formatHelp() {
|
|
|
415
747
|
"",
|
|
416
748
|
"Usage:",
|
|
417
749
|
" leroy connect [--api-key lr_live_...]",
|
|
418
|
-
" leroy
|
|
750
|
+
" leroy evaluate SYMBOL [--side buy|sell|both] [--setup] [--verbose] [--demo] [--horizon 5|15|30|60]",
|
|
751
|
+
" leroy watch SYMBOL [--side buy|sell|both] [--interval SECONDS] [--demo] [--horizon 5|15|30|60]",
|
|
752
|
+
" Buy is the default side; use --side sell or --side both to override it.",
|
|
419
753
|
"",
|
|
420
754
|
"Examples:",
|
|
421
|
-
" leroy
|
|
422
|
-
" leroy
|
|
423
|
-
" leroy
|
|
755
|
+
" leroy evaluate ABNB",
|
|
756
|
+
" leroy evaluate ABNB --setup --verbose",
|
|
757
|
+
" leroy evaluate AAPL --side sell --horizon 30",
|
|
758
|
+
" leroy evaluate ABNB --json",
|
|
759
|
+
" leroy evaluate ABNB --demo",
|
|
760
|
+
" leroy watch ABNB",
|
|
761
|
+
"",
|
|
762
|
+
"Read the full CLI reference: https://getleroy.com/cli",
|
|
424
763
|
].join("\n");
|
|
425
764
|
}
|