@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 CHANGED
@@ -6,9 +6,11 @@ The CLI is the command-line surface for Leroy's research-only MCP service.
6
6
 
7
7
  ```bash
8
8
  npm install --global @leroylabs/cli
9
- leroy connect
10
9
  ```
11
10
 
11
+ After installation, run `leroy connect` when prompted to connect the CLI to
12
+ your Leroy account.
13
+
12
14
  `leroy connect` validates the endpoint and stores the API key in
13
15
  `~/.config/leroy/config.json` with owner-only permissions. The CLI never
14
16
  submits orders and never has broker authority.
@@ -19,7 +21,9 @@ From the repository root:
19
21
 
20
22
  ```bash
21
23
  npm run leroy -- help
22
- npm run leroy -- ask buy ABNB
24
+ npm run leroy -- evaluate ABNB
25
+ npm run leroy -- watch ABNB
26
+ npm run leroy -- evaluate ABNB --demo
23
27
  ```
24
28
 
25
29
  The command reads `LEROY_MCP_URL` and `LEROY_API_KEY` when present. Otherwise,
@@ -28,11 +32,45 @@ Leroy config.
28
32
 
29
33
  The default endpoint is `https://getleroy.com/api/mcp`.
30
34
 
31
- The CLI supports the authenticated `evaluate_current_setup` MCP tool. Use
32
- `--json` for agent and shell integrations, or the default terminal format for
33
- human-readable evidence. A normal request reports current-frame freshness,
34
- the registered and evaluated strategy counts, exact versus broader Market
35
- Memory evidence, completed-label counts, and the research-only boundary.
35
+ `evaluate` and `watch` default to the buy case. Use `--side sell` or
36
+ `--side both` when you need to evaluate a different side.
37
+
38
+ Use `--demo` with `evaluate` or `watch` to render local randomized evidence without
39
+ an API key or network request. Each demo evaluation generates 41 individual
40
+ strategy cells plus varied market context, match strength, and outcomes so the
41
+ interactive UI can be explored outside market hours.
42
+
43
+ The CLI supports the authenticated `evaluate_current_setup` MCP tool. In an
44
+ interactive terminal, the default `leroy evaluate` view renders the evidence card
45
+ immediately and replaces its loading state with the authenticated result. Use
46
+ `--json` for agent and shell integrations, or `--setup`/`--verbose` when you
47
+ need the deterministic text formatter. A normal request reports current-frame
48
+ freshness, the registered and evaluated strategy counts, exact versus broader
49
+ Market Memory evidence, completed-label counts, and the research-only
50
+ boundary.
51
+
52
+ Use `leroy watch ABNB` in an interactive terminal to keep the evidence view
53
+ open while the current tape advances. It refreshes once per minute by default,
54
+ which matches the one-minute tape cadence; use `--interval SECONDS` to choose a
55
+ different polling interval. Each refresh is an authenticated evaluation request
56
+ and may count toward the account's lookup allowance.
57
+
58
+ ## Match strength
59
+
60
+ Leroy selects the strongest available evidence tier and does not pool lower
61
+ tiers into it:
62
+
63
+ - `5/5`: exact ticker setup
64
+ - `4/5`: same setup on other tickers
65
+ - `3/5`: same 41-sensor state
66
+ - `2/5`: same side with strategy and market context
67
+ - `1/5`: same side baseline
68
+
69
+ The human-readable output shows `Exact Matches` only for `5/5`. Lower tiers
70
+ show only their own `Comparable Matches` count. Use `--verbose` for the match
71
+ basis, or `--json` for the complete structured response.
72
+
73
+ The browser reference is available at [getleroy.com/cli](https://getleroy.com/cli).
36
74
 
37
75
  ## Exit codes
38
76
 
package/bin/leroy.mjs CHANGED
@@ -2,12 +2,28 @@
2
2
 
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
+ import React from "react";
5
6
 
6
7
  import { parseArgs } from "../src/args.mjs";
7
8
  import { LeroyMcpClient } from "../src/client.mjs";
8
9
  import { configPath, resolvedConfig, writeConfig } from "../src/config.mjs";
10
+ import { createDemoResponse } from "../src/demo.mjs";
9
11
  import { usageError } from "../src/errors.mjs";
10
12
  import { formatEvaluation, formatHelp } from "../src/format.mjs";
13
+ import { LiveEvidenceApp, render } from "../src/live-ui.mjs";
14
+ import { ansiForeground, LEROY_COLORS } from "../src/colors.mjs";
15
+
16
+ const ANSI = {
17
+ reset: "\u001b[0m",
18
+ bold: "\u001b[1m",
19
+ white: "\u001b[37m",
20
+ green: ansiForeground(LEROY_COLORS.green),
21
+ neutral: ansiForeground(LEROY_COLORS.neutral),
22
+ };
23
+
24
+ function promptStyle(value, tones, enabled) {
25
+ return enabled ? `${tones.map((tone) => ANSI[tone]).join("")}${value}${ANSI.reset}` : value;
26
+ }
11
27
 
12
28
  function print(value = "") {
13
29
  output.write(`${value}\n`);
@@ -17,9 +33,15 @@ async function connect(options) {
17
33
  const config = await resolvedConfig(options);
18
34
  let apiKey = config.apiKey;
19
35
  if (!apiKey && input.isTTY) {
20
- print("Create or activate an MCP key at https://getleroy.com/mcp.");
36
+ const colorEnabled = output.isTTY && !options.noColor;
37
+ print();
38
+ print(`${promptStyle("▲", ["green"], colorEnabled)} ${promptStyle("Connect Leroy", ["bold", "white"], colorEnabled)}`);
39
+ print();
40
+ print(` ${promptStyle("Create or activate an MCP key at", ["neutral"], colorEnabled)}`);
41
+ print(` ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
42
+ print();
21
43
  const readline = createInterface({ input, output });
22
- apiKey = (await readline.question("Paste your lr_live_ key: ")).trim();
44
+ apiKey = (await readline.question(` ${promptStyle("›", ["green"], colorEnabled)} Paste your ${promptStyle("lr_live_", ["bold", "white"], colorEnabled)} key: `)).trim();
23
45
  readline.close();
24
46
  }
25
47
  if (!apiKey) {
@@ -34,22 +56,76 @@ async function connect(options) {
34
56
  return 0;
35
57
  }
36
58
 
37
- async function ask(options) {
38
- const config = await resolvedConfig(options);
39
- if (!config.apiKey) {
40
- throw usageError("No API key found. Run `leroy connect` or set LEROY_API_KEY.");
41
- }
42
- const client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
43
- const response = await client.evaluate({
59
+ async function evaluate(options) {
60
+ const request = {
44
61
  symbol: options.symbol,
45
62
  side: options.side,
46
63
  holding_horizons: options.horizons,
47
64
  entry_price: options.entryPrice,
48
65
  stop_price: options.stopPrice,
49
66
  target_price: options.targetPrice,
50
- });
67
+ };
68
+ let client;
69
+ if (options.demo) {
70
+ client = { evaluate: async () => createDemoResponse(request) };
71
+ } else {
72
+ const config = await resolvedConfig(options);
73
+ if (!config.apiKey) {
74
+ throw usageError("No API key found. Run `leroy connect` or set LEROY_API_KEY.");
75
+ }
76
+ client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
77
+ }
78
+ if (input.isTTY && output.isTTY && !options.json && !options.setup && !options.verbose) {
79
+ const app = render(React.createElement(LiveEvidenceApp, {
80
+ client,
81
+ request,
82
+ colorEnabled: !options.noColor,
83
+ polling: false,
84
+ }), { exitOnCtrlC: true });
85
+ await app.waitUntilExit();
86
+ return 0;
87
+ }
88
+ const response = await client.evaluate(request);
51
89
  if (options.json) print(JSON.stringify(response, null, 2));
52
- else print(formatEvaluation(response, { colorEnabled: !options.noColor && output.isTTY, horizon: options.horizons.includes(30) ? 30 : options.horizons[options.horizons.length - 1] }));
90
+ else print(formatEvaluation(response, {
91
+ colorEnabled: !options.noColor && output.isTTY,
92
+ horizon: options.horizons.includes(30) ? 30 : options.horizons[options.horizons.length - 1],
93
+ setup: options.setup,
94
+ verbose: options.verbose,
95
+ layout: "grid",
96
+ }));
97
+ return 0;
98
+ }
99
+
100
+ async function watch(options) {
101
+ if (!input.isTTY || !output.isTTY) {
102
+ throw usageError("`leroy watch` requires an interactive terminal. Use `leroy evaluate` for one-shot output.");
103
+ }
104
+ const request = {
105
+ symbol: options.symbol,
106
+ side: options.side,
107
+ holding_horizons: options.horizons,
108
+ ...(Number.isFinite(options.entryPrice) ? { entry_price: options.entryPrice } : {}),
109
+ ...(Number.isFinite(options.stopPrice) ? { stop_price: options.stopPrice } : {}),
110
+ ...(Number.isFinite(options.targetPrice) ? { target_price: options.targetPrice } : {}),
111
+ };
112
+ let client;
113
+ if (options.demo) {
114
+ client = { evaluate: async () => createDemoResponse(request) };
115
+ } else {
116
+ const config = await resolvedConfig(options);
117
+ if (!config.apiKey) {
118
+ throw usageError("No API key found. Run `leroy connect` or set LEROY_API_KEY.");
119
+ }
120
+ client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
121
+ }
122
+ const app = render(React.createElement(LiveEvidenceApp, {
123
+ client,
124
+ request,
125
+ colorEnabled: !options.noColor,
126
+ intervalMs: options.intervalSeconds * 1_000,
127
+ }), { exitOnCtrlC: true });
128
+ await app.waitUntilExit();
53
129
  return 0;
54
130
  }
55
131
 
@@ -61,7 +137,8 @@ async function main() {
61
137
  return 0;
62
138
  }
63
139
  if (options.command === "connect") return await connect(options);
64
- if (options.command === "ask") return await ask(options);
140
+ if (options.command === "evaluate") return await evaluate(options);
141
+ if (options.command === "watch") return await watch(options);
65
142
  throw usageError("Unknown command: " + options.command + ". Run `leroy help`.");
66
143
  } catch (error) {
67
144
  console.error(`leroy: ${error instanceof Error ? error.message : String(error)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leroylabs/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Command-line access to Leroy market evidence.",
5
5
  "type": "module",
6
6
  "files": [
@@ -19,6 +19,11 @@
19
19
  "bin": {
20
20
  "leroy": "bin/leroy.mjs"
21
21
  },
22
+ "dependencies": {
23
+ "cli-table3": "^0.6.5",
24
+ "ink": "^7.1.1",
25
+ "react": "^19.2.8"
26
+ },
22
27
  "engines": {
23
28
  "node": ">=22"
24
29
  }
package/src/args.mjs CHANGED
@@ -18,11 +18,16 @@ export function parseArgs(argv) {
18
18
  apiKey: null,
19
19
  json: false,
20
20
  noColor: false,
21
+ setup: false,
22
+ verbose: false,
23
+ demo: false,
21
24
  entryPrice: null,
22
25
  stopPrice: null,
23
26
  targetPrice: null,
27
+ intervalSeconds: 60,
24
28
  };
25
29
  const positional = [];
30
+ let sideFlagProvided = false;
26
31
  for (let index = 0; index < rest.length; index += 1) {
27
32
  const value = rest[index];
28
33
  if (!value.startsWith("--")) {
@@ -30,28 +35,63 @@ export function parseArgs(argv) {
30
35
  continue;
31
36
  }
32
37
  const [flag, inline] = value.split("=", 2);
38
+ if (flag === "--json") {
39
+ options.json = true;
40
+ continue;
41
+ }
42
+ if (flag === "--no-color") {
43
+ options.noColor = true;
44
+ continue;
45
+ }
46
+ if (flag === "--setup") {
47
+ options.setup = true;
48
+ continue;
49
+ }
50
+ if (flag === "--verbose") {
51
+ options.verbose = true;
52
+ continue;
53
+ }
54
+ if (flag === "--demo") {
55
+ options.demo = true;
56
+ continue;
57
+ }
58
+ if (flag === "--help" || flag === "-h") {
59
+ options.command = "help";
60
+ continue;
61
+ }
33
62
  const next = inline ?? rest[++index];
34
- if (flag === "--side") options.side = next;
63
+ if (flag === "--side") {
64
+ options.side = next;
65
+ sideFlagProvided = true;
66
+ }
35
67
  else if (flag === "--horizon") options.horizons = [Number(next)];
36
68
  else if (flag === "--endpoint") options.endpoint = next;
37
69
  else if (flag === "--api-key") options.apiKey = next;
38
70
  else if (flag === "--entry-price") options.entryPrice = numberValue(next, flag);
39
71
  else if (flag === "--stop-price") options.stopPrice = numberValue(next, flag);
40
72
  else if (flag === "--target-price") options.targetPrice = numberValue(next, flag);
41
- else if (flag === "--json") options.json = true;
42
- else if (flag === "--no-color") options.noColor = true;
43
- else if (flag === "--help" || flag === "-h") options.command = "help";
73
+ else if (flag === "--interval") options.intervalSeconds = numberValue(next, flag);
44
74
  else throw usageError(`Unknown option: ${flag}`);
45
75
  }
46
- if (command === "ask" && ["buy", "sell", "both"].includes(positional[0]?.toLowerCase())) {
47
- if (options.side === "buy") options.side = positional[0].toLowerCase();
76
+ if (["evaluate", "watch"].includes(command) && ["buy", "sell", "both"].includes(positional[0]?.toLowerCase())) {
77
+ const positionalSide = positional[0].toLowerCase();
78
+ if (sideFlagProvided && options.side !== positionalSide) {
79
+ throw usageError("Specify the side once, using either a positional value or --side.");
80
+ }
81
+ if (!sideFlagProvided) options.side = positionalSide;
48
82
  positional.shift();
49
83
  }
50
84
  options.symbol = positional[0] ? positional[0].toUpperCase() : null;
51
- if (options.command === "ask") {
52
- if (!options.symbol || !/^[A-Z][A-Z0-9.-]{0,11}$/.test(options.symbol)) throw usageError("Usage: leroy ask buy|sell|both SYMBOL");
85
+ if (["evaluate", "watch"].includes(options.command)) {
86
+ if (!options.symbol || !/^[A-Z][A-Z0-9.-]{0,11}$/.test(options.symbol)) {
87
+ throw usageError(`Usage: leroy ${options.command} SYMBOL [--side buy|sell|both]`);
88
+ }
53
89
  if (!["buy", "sell", "both"].includes(options.side)) throw usageError("--side must be buy, sell, or both.");
54
90
  if (!options.horizons.every((horizon) => DEFAULT_HORIZONS.includes(horizon))) throw usageError("--horizon must be 5, 15, 30, or 60.");
91
+ if (options.command === "watch" && options.json) throw usageError("`leroy watch` cannot be combined with --json.");
92
+ }
93
+ if (options.demo && !["evaluate", "watch"].includes(options.command)) {
94
+ throw usageError("`--demo` can only be used with `leroy evaluate` or `leroy watch`.");
55
95
  }
56
96
  return options;
57
97
  }
package/src/client.mjs CHANGED
@@ -39,6 +39,7 @@ export class LeroyMcpClient {
39
39
  this.fetchImpl = fetchImpl;
40
40
  this.sessionId = null;
41
41
  this.requestId = 0;
42
+ this.initialized = false;
42
43
  }
43
44
 
44
45
  async request(method, params = {}, { notification = false } = {}) {
@@ -70,31 +71,39 @@ export class LeroyMcpClient {
70
71
  }
71
72
 
72
73
  async initialize() {
74
+ if (this.initialized) return null;
73
75
  const envelope = await this.request("initialize", {
74
76
  protocolVersion: MCP_PROTOCOL_VERSION,
75
77
  capabilities: {},
76
78
  clientInfo: { name: "leroy-cli", version: "0.1.0" },
77
79
  });
78
80
  await this.request("notifications/initialized", {}, { notification: true }).catch(() => undefined);
81
+ this.initialized = true;
79
82
  return envelope;
80
83
  }
81
84
 
82
85
  async evaluate({ symbol, side, holding_horizons = [5, 15, 30, 60], entry_price, stop_price, target_price }) {
83
86
  await this.initialize();
84
- const envelope = await this.request("tools/call", {
85
- name: "evaluate_current_setup",
86
- arguments: {
87
- symbol: symbol.trim().toUpperCase(),
88
- side,
89
- holding_horizons,
90
- ...(Number.isFinite(entry_price) ? { entry_price } : {}),
91
- ...(Number.isFinite(stop_price) ? { stop_price } : {}),
92
- ...(Number.isFinite(target_price) ? { target_price } : {}),
93
- client_request_id: `leroy-cli-${randomUUID()}`,
94
- },
95
- });
96
- const content = structuredContent(envelope);
97
- if (!content) throw new Error("Leroy returned an MCP response without structured evaluation data.");
98
- return content;
87
+ try {
88
+ const envelope = await this.request("tools/call", {
89
+ name: "evaluate_current_setup",
90
+ arguments: {
91
+ symbol: symbol.trim().toUpperCase(),
92
+ side,
93
+ holding_horizons,
94
+ ...(Number.isFinite(entry_price) ? { entry_price } : {}),
95
+ ...(Number.isFinite(stop_price) ? { stop_price } : {}),
96
+ ...(Number.isFinite(target_price) ? { target_price } : {}),
97
+ client_request_id: `leroy-cli-${randomUUID()}`,
98
+ },
99
+ });
100
+ const content = structuredContent(envelope);
101
+ if (!content) throw new Error("Leroy returned an MCP response without structured evaluation data.");
102
+ return content;
103
+ } catch (error) {
104
+ this.initialized = false;
105
+ this.sessionId = null;
106
+ throw error;
107
+ }
99
108
  }
100
109
  }
package/src/colors.mjs ADDED
@@ -0,0 +1,14 @@
1
+ export const LEROY_COLORS = Object.freeze({
2
+ green: "#00D41B",
3
+ inactive: "#29332B",
4
+ neutral: "#8F9591",
5
+ red: "#F2562F",
6
+ });
7
+
8
+ export function ansiForeground(hex) {
9
+ const value = hex.replace(/^#/, "");
10
+ const red = Number.parseInt(value.slice(0, 2), 16);
11
+ const green = Number.parseInt(value.slice(2, 4), 16);
12
+ const blue = Number.parseInt(value.slice(4, 6), 16);
13
+ return `\u001b[38;2;${red};${green};${blue}m`;
14
+ }
package/src/demo.mjs ADDED
@@ -0,0 +1,144 @@
1
+ const DEMO_PRICE_BASES = {
2
+ ABNB: 183,
3
+ AAPL: 229,
4
+ AMZN: 231,
5
+ GOOG: 214,
6
+ META: 736,
7
+ MSFT: 505,
8
+ NVDA: 181,
9
+ TSLA: 339,
10
+ };
11
+
12
+ const MATCH_TIERS = [
13
+ null,
14
+ { match_kind: "side", scope: "all_market_memory_observations" },
15
+ { match_kind: "forward_overlay_context", scope: "forward_overlay_current_context" },
16
+ { match_kind: "strategy", scope: "exact_41_sensor_state" },
17
+ { match_kind: "cross_ticker_context", scope: "cross_ticker_exact_state" },
18
+ { match_kind: "context", scope: "same_ticker_exact" },
19
+ ];
20
+
21
+ function unit(rng) {
22
+ const value = Number(rng());
23
+ return Number.isFinite(value) ? Math.min(0.999999, Math.max(0, value)) : 0;
24
+ }
25
+
26
+ function randomInt(rng, minimum, maximum) {
27
+ return minimum + Math.floor(unit(rng) * (maximum - minimum + 1));
28
+ }
29
+
30
+ function randomBetween(rng, minimum, maximum, digits = 2) {
31
+ const value = minimum + unit(rng) * (maximum - minimum);
32
+ return Number(value.toFixed(digits));
33
+ }
34
+
35
+ function shuffled(values, rng) {
36
+ const result = [...values];
37
+ for (let index = result.length - 1; index > 0; index -= 1) {
38
+ const swapIndex = randomInt(rng, 0, index);
39
+ [result[index], result[swapIndex]] = [result[swapIndex], result[index]];
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function strategyStates(rng) {
45
+ const buy = randomInt(rng, 0, 18);
46
+ const sell = randomInt(rng, 0, Math.min(18, 41 - buy));
47
+ const neutral = 41 - buy - sell;
48
+ return shuffled([
49
+ ...Array.from({ length: buy }, () => "buy"),
50
+ ...Array.from({ length: neutral }, () => "neutral"),
51
+ ...Array.from({ length: sell }, () => "sell"),
52
+ ], rng);
53
+ }
54
+
55
+ function outcomeFor(rng, horizon, score) {
56
+ const direction = unit(rng) > 0.42 ? 1 : -1;
57
+ const magnitude = horizon / 60 * randomBetween(rng, 0.25, 1.35);
58
+ const mean = Number((direction * magnitude).toFixed(2));
59
+ const winRate = randomBetween(rng, direction > 0 ? 0.48 : 0.34, direction > 0 ? 0.72 : 0.56, 2);
60
+ const lift = Number((direction * randomBetween(rng, 1.5, 18)).toFixed(2));
61
+ return {
62
+ sample_size: score > 0 ? randomInt(rng, 40, 980000000) : 0,
63
+ mean_return_pct: mean,
64
+ win_rate: winRate,
65
+ lift_vs_baseline_bps: lift,
66
+ };
67
+ }
68
+
69
+ function matchFor(rng, score, outcomes) {
70
+ const tier = MATCH_TIERS[score];
71
+ if (!tier) return null;
72
+ const rowCount = score === 5
73
+ ? randomInt(rng, 250000000, 980000000)
74
+ : score === 4
75
+ ? randomInt(rng, 100000, 9000000)
76
+ : score === 3
77
+ ? randomInt(rng, 5000, 250000)
78
+ : score === 2
79
+ ? randomInt(rng, 1000, 50000)
80
+ : randomInt(rng, 250, 5000000000);
81
+ return {
82
+ ...tier,
83
+ row_count: rowCount,
84
+ first_session: "2010-03-31",
85
+ last_session: "2026-08-24",
86
+ outcomes,
87
+ };
88
+ }
89
+
90
+ export function createDemoResponse(request = {}, rng = Math.random) {
91
+ const symbol = typeof request.symbol === "string" ? request.symbol.toUpperCase() : "ABNB";
92
+ const side = ["buy", "sell", "both"].includes(request.side) ? request.side : "buy";
93
+ const score = randomInt(rng, 0, 5);
94
+ const states = strategyStates(rng);
95
+ const sourceTime = new Date(Date.now() - randomInt(rng, 0, 45) * 60_000).toISOString();
96
+ const basePrice = DEMO_PRICE_BASES[symbol] ?? randomBetween(rng, 35, 480);
97
+ const last = Number((basePrice + randomBetween(rng, -4.5, 4.5)).toFixed(2));
98
+ const outcomes = score > 0
99
+ ? Object.fromEntries([15, 30, 60].map((horizon) => [String(horizon), outcomeFor(rng, horizon, score)]))
100
+ : {};
101
+ const match = matchFor(rng, score, outcomes);
102
+ const marketContext = {
103
+ session_return_pct: randomBetween(rng, -4.2, 4.2),
104
+ vwap_distance_pct: randomBetween(rng, -2.4, 2.4),
105
+ atr_pct: randomBetween(rng, 0.05, 2.6),
106
+ volume_ratio: randomBetween(rng, 0.35, 2.1),
107
+ };
108
+ const benchmarkContext = {
109
+ session_return_pct: randomBetween(rng, -1.8, 1.8),
110
+ vwap_distance_pct: randomBetween(rng, -0.9, 0.9),
111
+ atr_pct: randomBetween(rng, 0.08, 1.4),
112
+ volume_ratio: randomBetween(rng, 0.55, 1.65),
113
+ };
114
+
115
+ return {
116
+ status: "evaluated",
117
+ request: { symbol, side, holding_horizons: request.holding_horizons ?? [5, 15, 30, 60] },
118
+ current_market_frame: {
119
+ symbol,
120
+ source_time: sourceTime,
121
+ market_session_status: unit(rng) > 0.5 ? "open" : "closed",
122
+ quote: { last },
123
+ market_context: marketContext,
124
+ benchmark: { symbol: "SPY", market_context: benchmarkContext },
125
+ },
126
+ strategy_states: states.map((state) => ({ state })),
127
+ historical_evidence: score > 0
128
+ ? [15, 30, 60].map((horizon_minutes) => ({
129
+ horizon_minutes,
130
+ availability: "available",
131
+ ...outcomes[String(horizon_minutes)],
132
+ }))
133
+ : [],
134
+ market_memory: {
135
+ status: score > 0 ? "historical_match" : "insufficient_context",
136
+ coverage: {
137
+ date_start: "2010-03-31",
138
+ date_end: "2026-08-24",
139
+ strategy_evaluations: 105447109561,
140
+ },
141
+ matches: match ? [{ side, matches: [match] }] : [],
142
+ },
143
+ };
144
+ }