dexterm-cli 0.1.7 → 0.2.1

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.
Files changed (233) hide show
  1. package/dist/packages/agents/src/spectator.js +31 -3
  2. package/dist/packages/agents/src/tiers.js +12 -0
  3. package/dist/packages/agents/src/tools.js +13 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +7 -2
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +5 -2
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +21 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  30. package/dist/packages/autotrade/src/rugproof.js +231 -0
  31. package/dist/packages/autotrade/src/runner.js +194 -28
  32. package/dist/packages/autotrade/src/service-guard.js +125 -0
  33. package/dist/packages/autotrade/src/service-install.js +215 -0
  34. package/dist/packages/autotrade/src/service-recover.js +232 -0
  35. package/dist/packages/autotrade/src/service.js +289 -0
  36. package/dist/packages/autotrade/src/sessions.js +16 -7
  37. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  38. package/dist/packages/autotrade/src/signal.js +196 -0
  39. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  40. package/dist/packages/autotrade/src/strategy.js +15 -2
  41. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  42. package/dist/packages/classifier/src/calibrate.js +138 -0
  43. package/dist/packages/classifier/src/classify.js +107 -0
  44. package/dist/packages/classifier/src/config.js +220 -0
  45. package/dist/packages/classifier/src/dossier.js +95 -0
  46. package/dist/packages/classifier/src/hardware.js +63 -0
  47. package/dist/packages/classifier/src/index.js +18 -0
  48. package/dist/packages/classifier/src/models.js +111 -0
  49. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  50. package/dist/packages/classifier/src/providers/http.js +63 -0
  51. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  52. package/dist/packages/classifier/src/providers/llm.js +89 -0
  53. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  54. package/dist/packages/classifier/src/questions.js +97 -0
  55. package/dist/packages/classifier/src/registry.js +42 -0
  56. package/dist/packages/classifier/src/snapshot.js +31 -0
  57. package/dist/packages/classifier/src/systemone.js +88 -0
  58. package/dist/packages/classifier/src/types.js +18 -0
  59. package/dist/packages/cli/assets/dash/dash.css +139 -0
  60. package/dist/packages/cli/assets/dash/dash.js +200 -0
  61. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  62. package/dist/packages/cli/assets/dash/index.html +36 -0
  63. package/dist/packages/cli/src/analyst/backend.js +3 -2
  64. package/dist/packages/cli/src/analyst/codex.js +3 -2
  65. package/dist/packages/cli/src/app-help-gen.js +26 -2
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  68. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
  70. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  71. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  72. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  73. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  74. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  75. package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
  76. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  77. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  78. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  79. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  80. package/dist/packages/cli/src/classifier-live.js +81 -0
  81. package/dist/packages/cli/src/dash/data.js +217 -0
  82. package/dist/packages/cli/src/dash/server.js +92 -0
  83. package/dist/packages/cli/src/dash-cmd.js +113 -0
  84. package/dist/packages/cli/src/doctor.js +34 -0
  85. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  86. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  87. package/dist/packages/cli/src/live/signals.js +8 -0
  88. package/dist/packages/cli/src/main.js +98 -6
  89. package/dist/packages/cli/src/mcp-follow.js +21 -1
  90. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  91. package/dist/packages/cli/src/narratives.js +114 -0
  92. package/dist/packages/cli/src/overlay/model.js +147 -0
  93. package/dist/packages/cli/src/overlay/page.js +146 -0
  94. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  95. package/dist/packages/cli/src/overlay/server.js +170 -0
  96. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  97. package/dist/packages/cli/src/pnl-card.js +4 -0
  98. package/dist/packages/cli/src/record-cmd.js +82 -11
  99. package/dist/packages/cli/src/rules-cmd.js +2 -1
  100. package/dist/packages/cli/src/rules.js +24 -4
  101. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  102. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  103. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  104. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  105. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  106. package/dist/packages/cli/src/screens/narrative.js +154 -2
  107. package/dist/packages/cli/src/state.js +1 -0
  108. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  109. package/dist/packages/cli/src/version.js +1 -1
  110. package/dist/packages/custom/src/rules.js +5 -1
  111. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  112. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  113. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  114. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  115. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  116. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  117. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  118. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  119. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  120. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  121. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  122. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  123. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  124. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  127. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  131. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  132. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  140. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  158. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  160. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  162. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  163. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  164. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  165. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  174. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  175. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  176. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  177. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  179. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  180. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  181. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  207. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  209. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  210. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  211. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  212. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  213. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  214. package/dist/packages/mcp/src/index.js +1 -1
  215. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  216. package/dist/packages/mcp/src/server.js +125 -10
  217. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  218. package/dist/packages/narratives/src/cluster.js +193 -0
  219. package/dist/packages/narratives/src/embed.js +184 -0
  220. package/dist/packages/narratives/src/export.js +60 -0
  221. package/dist/packages/narratives/src/index.js +10 -0
  222. package/dist/packages/narratives/src/label.js +48 -0
  223. package/dist/packages/narratives/src/normalize.js +179 -0
  224. package/dist/packages/narratives/src/recorder.js +93 -0
  225. package/dist/packages/narratives/src/tracker.js +328 -0
  226. package/dist/packages/recorder/src/exit-probes.js +215 -0
  227. package/dist/packages/recorder/src/index.js +1 -0
  228. package/dist/packages/recorder/src/recorder.js +11 -1
  229. package/dist/packages/recorder/src/retention.js +4 -0
  230. package/dist/packages/recorder/src/schema.js +20 -1
  231. package/dist/packages/tui/src/keymap.js +12 -0
  232. package/package.json +3 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -10,6 +10,19 @@
10
10
  // strategy_backtest observe a strategy or a proposal on recorded history (days <= 14), read-only, rate-limited
11
11
  // autotrade_status observe the paper runner's status file, strategies and pending proposals
12
12
  //
13
+ // task-1647 (0.2.0 card C), the JOURNEY draft -> backtested -> forward-tested -> always-on -> real:
14
+ //
15
+ // strategy_journey observe the ladder for one strategy: which rungs have evidence on these rules, what is next
16
+ // strategy_forward_report observe a forward test next to the backtest (held-out split) of the same rules
17
+ // strategy_forward_start control start a forward test: live data, paper, the strategy's OWN bankroll, a window
18
+ // (hours <= 72 or closed trades <= 200); only after a backtest; never real
19
+ // strategy_forward_stop control stop one (it flattens and writes its report; an early stop never counts)
20
+ // strategy_promote_propose propose a PROMOTION proposal (always-on or real) with an evidence card, never applied;
21
+ // no rung can be skipped; always-on is the user's y, real is the user's own
22
+ // caps + `signal arm` with a typed phrase on their terminal
23
+ // strategy_backtest now also runs the strategy on each half of the held-out split and records that as the
24
+ // strategy's backtest evidence (the engine's numbers on the recorded tape, never model text).
25
+ //
13
26
  // A proposal is a file in `$DEXTERM_HOME/strategies/.proposals/`. Nothing in this module writes a strategy:
14
27
  // the user applies a proposal on the Q screen with a keypress (a LOOSER one needs a second keypress, the
15
28
  // "less safe" popup), and that path re-checks everything here from scratch (`checkProposal`) instead of trusting
@@ -19,8 +32,9 @@
19
32
  //
20
33
  // @dexterm/autotrade imports @dexterm/mcp (the firewall), so the server loads the engine lazily (`loadEngine`);
21
34
  // the CLI hands its own static import to the same functions.
35
+ import { spawn } from 'node:child_process';
22
36
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
23
- import { join } from 'node:path';
37
+ import { basename, join } from 'node:path';
24
38
  import { randomBytes } from 'node:crypto';
25
39
  import { firewallText } from '../../data/src/index.js';
26
40
  import { firewall } from "./firewall.js";
@@ -42,6 +56,21 @@ export const AUTOTRADE_TOOLS = [...STRATEGY_READ_TOOLS, ...STRATEGY_PROPOSE_TOOL
42
56
  export const PARALLEL_READ_TOOLS = ['autotrade_parallel_list', 'autotrade_parallel_compare'];
43
57
  export const PARALLEL_CONTROL_TOOLS = ['autotrade_parallel_start', 'autotrade_parallel_stop'];
44
58
  export const PARALLEL_TOOLS = [...PARALLEL_READ_TOOLS, ...PARALLEL_CONTROL_TOOLS];
59
+ /** task-1647: the journey tools. Reads; forward-test control (paper, own bankroll); promotion PROPOSALS (never applied). */
60
+ export const JOURNEY_READ_TOOLS = ['strategy_journey', 'strategy_forward_report'];
61
+ export const JOURNEY_CONTROL_TOOLS = ['strategy_forward_start', 'strategy_forward_stop'];
62
+ export const JOURNEY_PROPOSE_TOOLS = ['strategy_promote_propose'];
63
+ export const JOURNEY_TOOLS = [...JOURNEY_READ_TOOLS, ...JOURNEY_CONTROL_TOOLS, ...JOURNEY_PROPOSE_TOOLS];
64
+ /** Forward tests a model may start per hour (each one is a background process on the user's machine). */
65
+ export const FORWARD_START_RATE = { perHour: 3 };
66
+ export const defaultForwardSpawner = (home, t, env) => {
67
+ const bin = process.argv[1] ?? '';
68
+ if (!/^dexterm(\.m?js)?$/.test(basename(bin)))
69
+ return null;
70
+ const child = spawn(process.execPath, [bin, 'autotrade', 'forward', 'run', t.forwardId], { detached: true, stdio: 'ignore', env: { ...env, DEXTERM_HOME: home } });
71
+ child.unref();
72
+ return child.pid ?? null;
73
+ };
45
74
  export const MAX_PROPOSAL_OPS = 20;
46
75
  export const BACKTEST_MAX_DAYS = 14;
47
76
  /** Backtests: one at a time, at least this far apart, and at most this many per hour per server process. */
@@ -56,7 +85,7 @@ const DAY_MS = 86_400_000;
56
85
  * to live, change the book, raise its own daily spend or lower the floor under which a bag counts as stuck.
57
86
  */
58
87
  export const STRATEGY_DENIED_FOR_MODELS = [
59
- { pointer: '/mode', why: 'only you can change the mode (a model can never switch a strategy to live)' },
88
+ { pointer: '/mode', why: 'only you can change the mode (a model can never switch a strategy to real or live; real is armed on your own terminal)' },
60
89
  { pointer: '/bankroll', why: 'only you can change the book' },
61
90
  { pointer: '/agent/budgetUsdPerDay', why: 'only you can change the daily spend (a model can never raise its own budget)' },
62
91
  { pointer: '/execution/minExitLiquidityUsd', why: 'only you can change the stuck-bag floor' },
@@ -690,6 +719,7 @@ export class AutotradeTools {
690
719
  backtestBusy = false;
691
720
  backtestTimes = [];
692
721
  proposalTimes = [];
722
+ forwardTimes = [];
693
723
  constructor(o = {}) {
694
724
  this.opts = o;
695
725
  this.env = o.env ?? process.env;
@@ -710,6 +740,11 @@ export class AutotradeTools {
710
740
  case 'strategy_propose_new': return await this.propose({ kind: 'new', id: String(args.id ?? ''), from: String(args.fromPreset ?? ''), name: args.name, ops: args.ops ?? [], why: args.why });
711
741
  case 'strategy_backtest': return await this.backtest(args);
712
742
  case 'autotrade_status': return await this.status();
743
+ case 'strategy_journey': return await this.journey(args);
744
+ case 'strategy_forward_start': return await this.forwardStart(args);
745
+ case 'strategy_forward_stop': return await this.forwardStop(args);
746
+ case 'strategy_forward_report': return await this.forwardReportTool(args);
747
+ case 'strategy_promote_propose': return await this.promotePropose(args);
713
748
  case 'autotrade_parallel_list': return await this.parallelList();
714
749
  case 'autotrade_parallel_compare': return await this.parallelCompare();
715
750
  case 'autotrade_parallel_start': return await this.parallelControl(args, 'start');
@@ -852,11 +887,21 @@ export class AutotradeTools {
852
887
  tape = at.tapeFromDoc(at.tapeFromDb(db, { since: until - days * DAY_MS, until, minPeakLiqUsd: 20_000, spanHours: 80 }));
853
888
  }
854
889
  const results = [];
890
+ let heldOut = null;
855
891
  for (const sub of subjects) {
856
- const { report } = await at.backtest(sub.s, tape);
857
- results.push({ label: sub.label, ...compactReport(report) });
892
+ const full = await at.backtest(sub.s, tape);
893
+ results.push({ label: sub.label, ...compactReport(full.report) });
894
+ // task-1647: a stored strategy's backtest is its journey evidence: the same rules on each half of the split.
895
+ if (args.id !== undefined) {
896
+ const { evidence } = await at.backtestEvidence(sub.s, tape, { by: this.actor, now: t, days, full });
897
+ at.recordBacktestEvidence(this.home(), evidence);
898
+ heldOut = { cutoff: evidence.heldOut.cutoff, train: evidence.heldOut.train, test: evidence.heldOut.test, verdict: evidence.verdict, claim: evidence.claim, rulesHash: evidence.rulesHash };
899
+ }
858
900
  }
859
- return { paper: true, readOnly: true, days, tape: { source: tape.source, mints: tape.mints.length, ticks: tape.ticks.length }, results, limits: at.DATA_LIMITS, note: 'A backtest on recorded history with modelled fills, not a prediction. Nothing was saved. Quote the trade count: small n means little.' };
901
+ return {
902
+ paper: true, readOnly: true, days, tape: { source: tape.source, mints: tape.mints.length, ticks: tape.ticks.length }, results, ...(heldOut ? { heldOut } : {}), limits: at.DATA_LIMITS,
903
+ note: `A backtest on recorded history with modelled fills, not a prediction. ${heldOut ? 'The held-out split (the newest 30% of coins) is recorded as this strategy\'s backtest evidence for its journey; the strategy itself was not changed. Gain wording only when heldOut.claim is true.' : 'Nothing was saved.'} Quote the trade count: small n means little.`,
904
+ };
860
905
  }
861
906
  finally {
862
907
  this.backtestBusy = false;
@@ -873,6 +918,124 @@ export class AutotradeTools {
873
918
  pendingProposals: pending.map((p) => ({ proposalId: p.proposalId, id: p.id, kind: p.kind, riskDelta: p.riskDelta, createdAt: new Date(p.createdAt).toISOString() })),
874
919
  };
875
920
  }
921
+ // ───────────────────────────── the journey (task-1647) ─────────────────────────────
922
+ async userStrategy(id) {
923
+ const { at } = await this.eng();
924
+ if (typeof id !== 'string' || !at.STRATEGY_ID.test(id))
925
+ return { error: 'id must be a strategy id from strategy_list' };
926
+ const store = await this.store();
927
+ const got = store.get(id);
928
+ if (!got)
929
+ return { error: `no strategy ${id} (strategy_list shows the ids)` };
930
+ if (got.source !== 'user')
931
+ return { error: `${id} is a shipped preset: propose a copy first (strategy_propose_new), the user applies it, then work on the copy` };
932
+ if (!got.ok)
933
+ return { error: `${id} does not validate: ${got.errors.map(at.fmtIssue).join('; ')}` };
934
+ return { s: got.strategy, raw: store.raw(id) };
935
+ }
936
+ async journey(args) {
937
+ const { at } = await this.eng();
938
+ const r = await this.userStrategy(args.id);
939
+ if ('error' in r)
940
+ return r;
941
+ const home = this.home();
942
+ const v = at.journeyView(home, r.s, { armedHash: at.loadArmings(home)[r.s.id]?.strategyHash ?? null });
943
+ const fw = at.findForwardTest(home, r.s.id, this.now());
944
+ const pending = new at.PromotionFiles(home).list({ status: 'pending', id: r.s.id });
945
+ return {
946
+ paper: r.s.mode !== 'real', id: v.id, version: v.version, rulesHash: v.rulesHash, mode: v.mode, stage: v.stage,
947
+ rungs: v.rungs.map((x) => ({ stage: x.stage, ok: x.ok, detail: cleanText(x.detail, 300) })), next: v.next, notes: v.notes.map((n) => cleanText(n, 200)),
948
+ forward: fw ? { forwardId: fw.forwardId, state: at.liveState(fw, this.now()), window: fw.window, progress: fw.progress, counts: fw.rulesHash === v.rulesHash && fw.state === 'done' && fw.source.startsWith('live') } : null,
949
+ pendingPromotions: pending.map((p) => ({ proposalId: p.proposalId, to: p.to, actor: p.actor })),
950
+ note: 'The ladder is draft → backtested → forward-tested → always-on → real, one rung at a time, each on evidence measured on these exact rules. Backtest with strategy_backtest, forward-test with strategy_forward_start, then propose a promotion with strategy_promote_propose; the user applies it. Never claim a result the evidence does not show.',
951
+ };
952
+ }
953
+ async forwardStart(args) {
954
+ const { at } = await this.eng();
955
+ const r = await this.userStrategy(args.id);
956
+ if ('error' in r)
957
+ return r;
958
+ const home = this.home();
959
+ const t = this.now();
960
+ while (this.forwardTimes.length && t - this.forwardTimes[0] > 3_600_000)
961
+ this.forwardTimes.shift();
962
+ if (this.forwardTimes.length >= FORWARD_START_RATE.perHour)
963
+ return { error: `at most ${FORWARD_START_RATE.perHour} forward tests started per hour`, reason: 'rate-limited' };
964
+ const v = at.journeyView(home, r.s, {});
965
+ if (!v.backtest)
966
+ return { error: `${r.s.id} has no backtest with the held-out split on its current rules: run strategy_backtest {id} first (the ladder is backtest, then forward test)`, reason: 'skip-refused' };
967
+ const hours = args.hours === undefined ? null : Number(args.hours);
968
+ const trades = args.trades === undefined ? null : Number(args.trades);
969
+ // No bankroll, no mode, no source: a model's forward test is always paper, live data, the strategy's own bankroll.
970
+ const c = at.createForwardTest(home, r.s, r.raw, { hours: hours ?? (trades === null ? 24 : null), trades, by: this.actor, now: t, source: 'live' });
971
+ if (!c.ok)
972
+ return { error: cleanText(c.why, 300) };
973
+ const pid = (this.opts.forwardSpawner ?? defaultForwardSpawner)(home, c.test, this.env);
974
+ if (!pid) {
975
+ at.saveForwardTest(home, { ...c.test, state: 'failed', endedAt: t, endReason: 'could not start the host from the chat' });
976
+ return { error: `could not start the forward test from here; the user can start it in a terminal: dexterm autotrade forward start ${r.s.id}${hours ? ` --hours ${hours}` : ''}${trades ? ` --trades ${trades}` : ''}` };
977
+ }
978
+ this.forwardTimes.push(t);
979
+ return {
980
+ paper: true, started: true, forwardId: c.test.forwardId, id: r.s.id, version: c.test.version, window: c.test.window, bankrollUsd: c.test.bankrollUsd,
981
+ note: `A FORWARD TEST runs in the background on LIVE data, on paper, with ${r.s.id}'s own bankroll and its own run file, until its window ends (${at.windowText(c.test.window)}). Read it with strategy_forward_report; stop it with strategy_forward_stop. Say nothing about results until it ends, and then only with the trade counts: under 20 closed trades it proves nothing.`,
982
+ };
983
+ }
984
+ async forwardRef(args) {
985
+ const { at } = await this.eng();
986
+ const ref = args.forwardId !== undefined ? String(args.forwardId) : args.id !== undefined ? String(args.id) : '';
987
+ if (!ref)
988
+ return { error: 'pass id (a strategy id) or forwardId' };
989
+ const t = at.findForwardTest(this.home(), ref, this.now());
990
+ return t ? { t } : { error: `no forward test for ${cleanText(ref, 40)}` };
991
+ }
992
+ async forwardStop(args) {
993
+ const { at } = await this.eng();
994
+ const f = await this.forwardRef(args);
995
+ if ('error' in f)
996
+ return f;
997
+ const r = at.requestForwardStop(this.home(), f.t.forwardId, this.actor, this.now());
998
+ if (!r.ok)
999
+ return { error: cleanText(r.why, 200) };
1000
+ return { paper: true, stopRequested: true, already: r.already, forwardId: f.t.forwardId, note: 'It flattens at its next tick, writes its report and ends. A forward test stopped early is shown in the journey but does not count as the forward-tested rung.' };
1001
+ }
1002
+ async forwardReportTool(args) {
1003
+ const { at } = await this.eng();
1004
+ const f = await this.forwardRef(args);
1005
+ if ('error' in f)
1006
+ return f;
1007
+ const r = at.forwardReport(this.home(), f.t, this.now());
1008
+ return {
1009
+ paper: true, readOnly: true, forwardId: r.forwardId, id: r.strategyId, version: r.version, state: r.state, source: r.source, window: r.window, elapsed: r.elapsed,
1010
+ endReason: r.endReason ? cleanText(r.endReason, 200) : null, progress: r.progress, metrics: r.metrics,
1011
+ comparison: { rows: r.comparison.rows, verdict: r.comparison.verdict, claim: r.comparison.claim, sampleOk: r.comparison.sampleOk },
1012
+ note: r.comparison.claim ? 'Both halves of the evidence agree; still quote the trade counts and say it is paper with modelled fills.' : 'No performance claim: report the numbers with their trade counts and the verdict lines as they are. Paper, modelled fills, not financial advice.',
1013
+ };
1014
+ }
1015
+ async promotePropose(args) {
1016
+ const { at } = await this.eng();
1017
+ const r = await this.userStrategy(args.id);
1018
+ if ('error' in r)
1019
+ return r;
1020
+ const t = this.now();
1021
+ while (this.proposalTimes.length && t - this.proposalTimes[0] > 600_000)
1022
+ this.proposalTimes.shift();
1023
+ if (this.proposalTimes.length >= 10)
1024
+ return { error: 'proposal rate limit: at most 10 proposals per 10 minutes', reason: 'rate-limited' };
1025
+ const home = this.home();
1026
+ const p = at.proposePromotion(home, r.s, String(args.to ?? ''), { actor: this.actor, why: cleanText(args.why), now: t, armedHash: at.loadArmings(home)[r.s.id]?.strategyHash ?? null });
1027
+ if (!p.ok)
1028
+ return { ok: false, applied: false, reason: p.reason, error: cleanText(p.why, 300), note: p.reason === 'skip-refused' ? 'No rung can be skipped: do the missing step first (strategy_journey says which).' : p.reason === 'evidence-floor' ? 'Nothing was proposed. Real needs the evidence floor: a COMPLETED forward test of these exact rules with the user\'s minimum of closed paper trades, and a backtest on record. The floor is the user\'s limit: you cannot propose past it, lower it, skip it or override it. Say what is missing and stop.' : 'Nothing was proposed.' };
1029
+ if (!p.duplicate)
1030
+ this.proposalTimes.push(t);
1031
+ const pr = p.proposal;
1032
+ return {
1033
+ ok: true, applied: false, proposalId: pr.proposalId, id: pr.id, to: pr.to, version: pr.version, card: pr.card, duplicate: p.duplicate,
1034
+ note: pr.to === 'real'
1035
+ ? `This is a PROPOSAL and it is NOT applied. Real mode is the user's alone: they set their own caps (dexterm autotrade signal caps set) and arm it on their own terminal with a typed phrase (dexterm autotrade promote apply ${pr.proposalId}, which hands over to signal arm). You cannot apply it, set or suggest cap values, or arm anything.`
1036
+ : `This is a PROPOSAL and it is NOT applied. The user applies it with y on a real terminal (dexterm autotrade promote apply ${pr.proposalId}) or on the Q screen journey view; then the always-on service runs it on paper. You cannot apply it.`,
1037
+ };
1038
+ }
876
1039
  // ───────────────────────────── parallel strategies (task-1529) ─────────────────────────────
877
1040
  /** autotrade.json's strategies and, while `run --all` runs, each one's state. */
878
1041
  async parallelList() {
@@ -14,7 +14,7 @@
14
14
  // never raise a cap, arm a mandate or turn live on, and no tool, MCP call or chat message changes the profile.
15
15
  import { ANALYST_ACTION_TYPES } from '../../../data/src/index.js';
16
16
  import { PAPER_NOTE_TOOLS, PAPER_READ_TOOLS, PAPER_WRITE_TOOLS, READ_ONLY_TOOLS } from "../server.js";
17
- import { PARALLEL_CONTROL_TOOLS, STRATEGY_PROPOSE_TOOLS } from "../autotrade-tools.js";
17
+ import { JOURNEY_CONTROL_TOOLS, JOURNEY_PROPOSE_TOOLS, PARALLEL_CONTROL_TOOLS, STRATEGY_PROPOSE_TOOLS } from "../autotrade-tools.js";
18
18
  import { LEDGER_PROPOSE_TOOLS } from "../ledger-tools.js";
19
19
  export const RESTRICTION_LEVELS = ['observe', 'propose', 'paper', 'full'];
20
20
  export const RESTRICTION_RANK = { observe: 0, propose: 1, paper: 2, full: 3 };
@@ -65,7 +65,7 @@ export function engineToolAllowlist(level, actionTypes) {
65
65
  if (!actionTypes || actionTypes.includes(t))
66
66
  out.add(t);
67
67
  if (allows(level, 'strategy-proposals') && !observeOnly)
68
- for (const t of STRATEGY_PROPOSE_TOOLS)
68
+ for (const t of [...STRATEGY_PROPOSE_TOOLS, ...JOURNEY_PROPOSE_TOOLS])
69
69
  out.add(t);
70
70
  // Paper tools: the MCP server still refuses them unless the user armed a mandate and enabled agent paper trading.
71
71
  if (allows(level, 'paper-orders') && !observeOnly)
@@ -74,7 +74,7 @@ export function engineToolAllowlist(level, actionTypes) {
74
74
  // v0.1.7 integration: the other paper-side writes (start / stop / pause one listed parallel strategy, a paper
75
75
  // journal tag, a backtest request file) join at `paper`, so `paper` and `full` offer every tool the claude path has
76
76
  if (allows(level, 'paper-orders') && !observeOnly)
77
- for (const t of [...PARALLEL_CONTROL_TOOLS, ...PAPER_NOTE_TOOLS, ...LEDGER_PROPOSE_TOOLS])
77
+ for (const t of [...PARALLEL_CONTROL_TOOLS, ...JOURNEY_CONTROL_TOOLS, ...PAPER_NOTE_TOOLS, ...LEDGER_PROPOSE_TOOLS])
78
78
  out.add(t);
79
79
  return [...out].filter((n) => !NEVER_TOOL.test(n));
80
80
  }
@@ -7,7 +7,7 @@ export { resolvePaperAgentDesk, paperAgentDeskDb } from "./paper-agent.js";
7
7
  export { OverlayStore, mergeOverlay, loadBundled, checkKb, checkSkill, moneyToolMentions, isMoneyToolName, parseFrontmatter, dextermHome, userKbDir, userSkillsDir, userDir, contentJournalPath, journalContent, writeOverlayFile, resetOverlayFile, templateFor, KB_LIMITS, SKILL_LIMITS, KB_ID, SKILL_NAME, KB_RULE, MONEY_TOOLS, } from "./user-content.js";
8
8
  // task-1507: the AUTOTRADE chat tools (strategy reads + proposals), the model deny-list, the risk delta and the
9
9
  // proposal files the Q screen's keypress path re-checks and applies.
10
- export { AutotradeTools, ProposalFiles, checkProposal, riskDelta, diffLines, screenDelta, deniedChanges, opsPolicyErrors, opsShapeErrors, proposalCardView, proposalsDir, isProposalId, cleanText, loadEngine, STRATEGY_DENIED_FOR_MODELS, STRATEGY_READ_TOOLS, STRATEGY_PROPOSE_TOOLS, AUTOTRADE_TOOLS, PARALLEL_READ_TOOLS, PARALLEL_CONTROL_TOOLS, PARALLEL_TOOLS, MAX_PROPOSAL_OPS, BACKTEST_MAX_DAYS, BACKTEST_RATE, PROPOSAL_SCHEMA, RUNNER_STATUS_SCHEMA, } from "./autotrade-tools.js";
10
+ export { AutotradeTools, ProposalFiles, checkProposal, riskDelta, diffLines, screenDelta, deniedChanges, opsPolicyErrors, opsShapeErrors, proposalCardView, proposalsDir, isProposalId, cleanText, loadEngine, STRATEGY_DENIED_FOR_MODELS, STRATEGY_READ_TOOLS, STRATEGY_PROPOSE_TOOLS, AUTOTRADE_TOOLS, PARALLEL_READ_TOOLS, PARALLEL_CONTROL_TOOLS, PARALLEL_TOOLS, JOURNEY_READ_TOOLS, JOURNEY_CONTROL_TOOLS, JOURNEY_PROPOSE_TOOLS, JOURNEY_TOOLS, FORWARD_START_RATE, defaultForwardSpawner, MAX_PROPOSAL_OPS, BACKTEST_MAX_DAYS, BACKTEST_RATE, PROPOSAL_SCHEMA, RUNNER_STATUS_SCHEMA, } from "./autotrade-tools.js";
11
11
  export { buildDossier } from "./dossier.js";
12
12
  // task-1530: the trade-ledger tools (reads + the backtest request), and the request files the CLI runs or dismisses.
13
13
  export { LedgerTools, LEDGER_TOOLS, LEDGER_READ_TOOLS, LEDGER_PROPOSE_TOOLS, tradeView, filterFromArgs } from "./ledger-tools.js";
@@ -11,9 +11,17 @@
11
11
  // every candidate of a run (what the skips turned into)
12
12
  // telegram_status observe the Telegram connector's settings: enabled, paired chats (a count), approvals, notify
13
13
  // trading_status observe where live trading happens (task-1571: not in dexterm; it holds no key and places no
14
- // live order) and whether the panic switch is engaged
14
+ // live order), whether the panic switch is engaged, and real mode's state (task-1645)
15
+ // trade_fire_feed observe task-1645: the MCP channel of real mode: the LIVE TRADE FIRE signals of the feed
16
+ // (autotrade/real/mcp-feed.jsonl) after a nonce, and real mode's status. Reading is all it
17
+ // does: no tool arms real mode, changes a cap, lifts the kill switch or records a receipt
18
+ // trading_limits observe task-1654: real mode's limits in force (for a strategy / account / mode), where each
19
+ // number comes from, what each limit does, today's counters and the executor's health
15
20
  // backtest_requests observe the backtests this agent asked for, and their results once the user ran them
16
21
  // backtest_request propose ask the user to run a longer backtest (a request file; the user runs or dismisses it)
22
+ // limits_propose propose task-1654: a limits change as a DIFF CARD (old -> new, which numbers it raises, the
23
+ // exact command the user would type). It writes nothing at all: only the user applies a
24
+ // change, on their own terminal (`dexterm autotrade limits set`; a raise is typed)
17
25
  //
18
26
  // Nothing here can trade, sign, change a cap, enable live or touch a key: the ledger is read through
19
27
  // `@dexterm/autotrade`'s loader (pure file reads, loaded lazily: @dexterm/autotrade imports this package). Text that
@@ -26,10 +34,10 @@ import { dextermHome } from "./user-content.js";
26
34
  import { BacktestRequests, MAX_PENDING_REQUESTS, REQUEST_MAX_DAYS, REQUEST_RATE_PER_HOUR } from "./backtest-requests.js";
27
35
  export const LEDGER_READ_TOOLS = [
28
36
  'ledger_trades', 'ledger_positions', 'ledger_equity', 'ledger_summary', 'ledger_compare',
29
- 'candidate_dossier', 'outcomes_lookup', 'telegram_status', 'trading_status', 'backtest_requests',
37
+ 'candidate_dossier', 'outcomes_lookup', 'telegram_status', 'trading_status', 'trade_fire_feed', 'trading_limits', 'backtest_requests',
30
38
  ];
31
39
  /** Write-shaped (a request file), so not on the read-only surface a skill may name. */
32
- export const LEDGER_PROPOSE_TOOLS = ['backtest_request'];
40
+ export const LEDGER_PROPOSE_TOOLS = ['backtest_request', 'limits_propose'];
33
41
  export const LEDGER_TOOLS = [...LEDGER_READ_TOOLS, ...LEDGER_PROPOSE_TOOLS];
34
42
  const MINT = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
35
43
  const RUN_ID = /^[0-9TZ]+-[a-z][a-z0-9-]{1,31}-[0-9a-f]{4}$/;
@@ -64,10 +72,10 @@ export function filterFromArgs(a, now) {
64
72
  return allowed.includes(a[k]) ? a[k] : false;
65
73
  };
66
74
  const source = pick('source', ['paper', 'desk', 'autotrade', 'live']);
67
- const mode = pick('mode', ['paper', 'devnet', 'live']);
75
+ const mode = pick('mode', ['paper', 'devnet', 'live', 'real']);
68
76
  const status = pick('status', ['open', 'closed']);
69
77
  if (source === false || mode === false || status === false)
70
- return { ok: false, error: 'source is paper|desk|autotrade|live, mode is paper|devnet|live, status is open|closed' };
78
+ return { ok: false, error: 'source is paper|desk|autotrade|live, mode is paper|devnet|live|real, status is open|closed' };
71
79
  if (source)
72
80
  f.source = source;
73
81
  if (mode)
@@ -135,6 +143,9 @@ export class LedgerTools {
135
143
  case 'outcomes_lookup': return await this.outcomes(args);
136
144
  case 'telegram_status': return this.telegram();
137
145
  case 'trading_status': return this.trading();
146
+ case 'trade_fire_feed': return this.signals(args);
147
+ case 'trading_limits': return await this.limits(args);
148
+ case 'limits_propose': return await this.limitsPropose(args);
138
149
  case 'backtest_requests': return this.requests();
139
150
  case 'backtest_request': return await this.request(args);
140
151
  default: return { error: `unknown ledger tool ${name}` };
@@ -154,7 +165,7 @@ export class LedgerTools {
154
165
  const rows = filterTrades(v.trades, filter);
155
166
  const integrity = { ledger: v.file.check.ok ? `verified, ${v.file.events.length} events` : `FAILS its check at line ${v.file.check.at}: ${v.file.check.error}`, pendingFromSources: v.pending.length, problems: v.sources.filter((s) => s.problem).map((s) => s.problem) };
156
167
  const modes = [...new Set(rows.map((t) => t.walletMode))];
157
- const base = { readOnly: true, modes, integrity, note: modes.length > 1 ? 'PAPER, DEVNET and LIVE trades are in this set: filter by mode before comparing results.' : modes.includes('paper') ? 'PAPER (simulated) trades: never present them as results.' : undefined };
168
+ const base = { readOnly: true, modes, integrity, note: modes.length > 1 ? 'PAPER, REAL, DEVNET and LIVE trades are in this set: filter by mode before comparing results.' : modes.includes('paper') ? 'PAPER (simulated) trades: never present them as results.' : modes.includes('real') ? 'REAL-mode trades: dexterm\'s model of the trades it signalled to the user\'s executor; the executor\'s receipts are the real fills.' : undefined };
158
169
  if (name === 'ledger_trades' || name === 'ledger_positions') {
159
170
  const limit = Math.min(200, Math.max(1, Number.isInteger(args.limit) ? Number(args.limit) : 50));
160
171
  const newest = [...rows].sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
@@ -292,13 +303,150 @@ export class LedgerTools {
292
303
  // ───────────────────────────── live trading (task-1571: not in dexterm) ───────────────
293
304
  trading() {
294
305
  const panic = existsSync(join(this.home(), 'trading.panic'));
306
+ const real = this.realState();
295
307
  return {
296
308
  readOnly: true,
297
- liveTrading: 'not in dexterm: dexterm holds no key, signs nothing and places no live order. Live trading happens in the Telegram trading bot the user chose, which holds its own wallet; dexterm only sends alerts and run control to the user\'s own bot (telegram_status).',
309
+ liveTrading: 'not in dexterm: dexterm holds no key, signs nothing and places no live order. Live trading happens in the Telegram trading bot (or script) the user chose, which holds its own wallet; dexterm only sends alerts, run control and, in real mode, LIVE TRADE FIRE signals to the user\'s own executor (telegram_status, trade_fire_feed).',
298
310
  paperTrading: 'local: the paper ticket, paper accounts and paper autotrade runs (ledger_* tools)',
311
+ realMode: `${real.armed.length} strategy(ies) armed for real mode by the user on their own terminal${real.kill ? `; the real-mode kill switch is ON (${real.kill})` : ''}. Only the user arms, disarms, sets caps or lifts the kill switch, on their own terminal; no tool can.`,
299
312
  panicSwitch: panic ? 'engaged (autotrade runs and the paper agent desk are halted; only `dexterm wallet resume` in a terminal clears it)' : 'off',
300
313
  };
301
314
  }
315
+ // ───────────────────────────── real mode (task-1645) ─────────────────────────────
316
+ readJson(path) {
317
+ try {
318
+ return JSON.parse(readFileSync(path, 'utf8'));
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ /** Real mode's state from its files (pure reads; ids and numbers only). */
325
+ realState() {
326
+ const dir = join(this.home(), 'autotrade', 'real');
327
+ const arming = this.readJson(join(dir, 'arming.json'));
328
+ const armed = Object.values((arming?.armed ?? {}))
329
+ .filter((a) => typeof a.strategyId === 'string' && /^[a-z][a-z0-9-]{1,31}$/.test(a.strategyId))
330
+ .map((a) => ({ id: String(a.strategyId), version: String(a.version ?? '').slice(0, 16), accountId: String(a.accountId ?? '').replace(/[^a-z0-9_-]/g, '').slice(0, 32) }));
331
+ const killFile = join(dir, 'kill.json');
332
+ const k = existsSync(killFile) ? this.readJson(killFile) : null;
333
+ // task-1654: the caps are four of the limits (limits.json global, else card A's caps.json).
334
+ const lim = this.readJson(join(dir, 'limits.json'))?.global;
335
+ const caps = (lim ? Object.fromEntries(['perTradeSol', 'dailySol', 'buysPerDay', 'maxOpenPositions'].filter((k) => typeof lim[k] === 'number').map((k) => [k, lim[k]])) : undefined) ?? this.readJson(join(dir, 'caps.json'))?.caps;
336
+ const capsNum = caps ? Object.fromEntries(Object.entries(caps).filter(([, v]) => typeof v === 'number')) : null;
337
+ const n = Number(this.readJson(join(dir, 'nonce.json'))?.last);
338
+ return { armed, kill: existsSync(killFile) ? cleanLedgerText(k?.reason ?? 'engaged', 80) || 'engaged' : null, caps: capsNum, lastNonce: Number.isSafeInteger(n) ? n : null };
339
+ }
340
+ signals(args) {
341
+ const after = Number(args.after_nonce ?? 0);
342
+ const limit = Number(args.limit ?? 50);
343
+ if (!Number.isSafeInteger(after) || after < 0)
344
+ return { error: 'after_nonce is a whole number >= 0' };
345
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
346
+ return { error: 'limit is a whole number from 1 to 100' };
347
+ const feed = join(this.home(), 'autotrade', 'real', 'mcp-feed.jsonl');
348
+ const out = [];
349
+ let feedOn = false;
350
+ if (existsSync(feed)) {
351
+ feedOn = true;
352
+ for (const l of readFileSync(feed, 'utf8').split('\n')) {
353
+ if (!l.trim())
354
+ continue;
355
+ let x;
356
+ try {
357
+ x = JSON.parse(l);
358
+ }
359
+ catch {
360
+ continue;
361
+ }
362
+ if (x.schema !== 'dexterm-trade-fire/1' || typeof x.nonce !== 'number' || x.nonce <= after)
363
+ continue;
364
+ // The signal as dexterm wrote it; the only token text in it is the symbol, already reduced to [A-Za-z0-9._$-].
365
+ out.push({ ...x, symbol: typeof x.symbol === 'string' ? x.symbol.replace(/[^A-Za-z0-9._$-]/g, '').slice(0, 16) : undefined });
366
+ if (out.length >= limit)
367
+ break;
368
+ }
369
+ }
370
+ const st = this.realState();
371
+ return {
372
+ readOnly: true, schema: 'dexterm-trade-fire/1', signals: out, nextAfterNonce: out.length ? out[out.length - 1].nonce : after,
373
+ realMode: { armed: st.armed, kill: st.kill, caps: st.caps, lastNonce: st.lastNonce, mcpFeed: feedOn ? 'on' : 'off (the user enables it: dexterm autotrade signal channels --mcp on)' },
374
+ rules: 'An executor acts only on its own accountId, ignores a signal after expiresAt, and dedupes by nonce (delivery is at-least-once). type "halted" ends a run; type "test" is never traded. The user records what was done with `dexterm autotrade receipt`; this tool changes nothing.',
375
+ };
376
+ }
377
+ // ───────────────────────────── limits (task-1654) ─────────────────────────────
378
+ limitsContext(args) {
379
+ const id = (v, name) => (v === undefined ? '-' : typeof v === 'string' && /^[a-z0-9][a-z0-9._-]{0,63}$/.test(v) ? v : { error: `${name} is an id ([a-z0-9._-], 64 max)` });
380
+ const strategyId = id(args.strategy, 'strategy');
381
+ if (typeof strategyId !== 'string')
382
+ return strategyId;
383
+ const accountId = id(args.account, 'account');
384
+ if (typeof accountId !== 'string')
385
+ return accountId;
386
+ if (args.mode !== undefined && args.mode !== 'real' && args.mode !== 'paper')
387
+ return { error: 'mode is real or paper' };
388
+ return { strategyId, accountId, mode: args.mode === 'paper' ? 'paper' : 'real' };
389
+ }
390
+ async limits(args) {
391
+ const c = this.limitsContext(args);
392
+ if ('error' in c)
393
+ return c;
394
+ const at = await import('../../autotrade/src/index.js');
395
+ const home = this.home();
396
+ const l = at.loadLimits(home);
397
+ const doc = l.ok ? l.doc : null;
398
+ const e = at.effectiveLimits(doc, c);
399
+ const risk = at.loadRiskState(home, this.now());
400
+ const ex = at.executorHealth(home, { now: this.now(), windowMin: e.values.executorSilenceMin ?? null });
401
+ return {
402
+ readOnly: true, context: c, applies: e.applies,
403
+ state: l.ok ? (l.source === 'migrated' ? 'migrated from caps.json' : l.unjournaled ? 'changed outside dexterm (a real run refuses it if it raises anything)' : 'saved by the user') : l.missing ? 'not set yet: placeholder defaults (real mode does not arm on them silently)' : 'unreadable: real buys are refused',
404
+ limits: at.LIMIT_FIELDS.map((f) => ({ key: f.key, group: f.group, unit: f.unit, kind: f.dir === 'max' ? 'cap (smaller is stricter)' : 'floor (larger is stricter)', value: e.values[f.key] ?? null, from: e.from[f.key] ?? null, placeholder: at.DEFAULT_LIMITS[f.key] ?? null, what: f.help })),
405
+ today: { day: risk.day, buys: risk.buys, buySol: risk.buySol, realizedSol: risk.dayPnlSol, lossStreak: risk.lossStreak, pausedUntil: iso(risk.pausedUntil), breaker: risk.breaker ? { at: iso(risk.breaker.at), drawdownSol: risk.breaker.drawdownSol } : null },
406
+ executor: at.healthLine(ex),
407
+ rules: 'Limits live outside the model. The effective number is the placeholder default replaced by the user\'s global value, then the STRICTER of every scope that sets it (mode, strategy, account). No tool changes them: limits_propose returns a diff card for the user to apply on their own terminal (raising needs their typed confirmation). The defaults are placeholders, not advice.',
408
+ };
409
+ }
410
+ async limitsPropose(args) {
411
+ const at = await import('../../autotrade/src/index.js');
412
+ const scope = typeof args.scope === 'string' ? args.scope : 'global';
413
+ const changes = args.changes;
414
+ if (!changes || typeof changes !== 'object' || Array.isArray(changes) || !Object.keys(changes).length || Object.keys(changes).length > 24)
415
+ return { error: 'changes is an object of 1 to 24 limits: { "perTradeSol": 0.1, "dailyLossSol": null }' };
416
+ // The agent's own reason is returned apart from the card, firewalled: the card is engine-made lines only.
417
+ const why = typeof args.why === 'string' ? fw(args.why.slice(0, 200), 'limits-why') : null;
418
+ const home = this.home();
419
+ const l = at.loadLimits(home);
420
+ const cur = l.ok ? l.doc : at.emptyLimits();
421
+ const next = structuredClone(cur);
422
+ delete next.migratedFrom;
423
+ const ref = at.scopeRef(next, scope, true);
424
+ if ('error' in ref)
425
+ return { error: ref.error };
426
+ for (const [k, v] of Object.entries(changes)) {
427
+ if (!at.limitField(k))
428
+ return { error: `unknown limit "${String(k).slice(0, 40)}"` };
429
+ if (v !== null && (typeof v !== 'number' || !Number.isFinite(v)))
430
+ return { error: `${k}: a number, or null to remove it` };
431
+ if (v === null && !ref.isGlobal)
432
+ delete ref.set[k];
433
+ else
434
+ ref.set[k] = v;
435
+ }
436
+ const issues = at.validateLimits(next);
437
+ if (issues.length)
438
+ return { error: issues.map((i) => `${i.path}: ${i.msg}`).join('; ').slice(0, 400) };
439
+ const diff = at.diffLimits(l.ok ? cur : null, next);
440
+ const up = at.raises(diff);
441
+ const assign = Object.entries(changes).map(([k, v]) => `${k}=${v === null ? 'off' : v}`).join(' ');
442
+ return {
443
+ applied: false, proposal: true,
444
+ card: [`PROPOSED LIMITS CHANGE · ${scope}`, ...diff.map((d) => ` ${at.changeLine(d)}`), up.length ? ` raises ${up.length} limit(s): you would type the confirmation on your own terminal` : ' lowers only: applies at once when you run it', ` to apply it yourself: dexterm autotrade limits set --scope ${scope} ${assign}`],
445
+ changes: diff, raises: up.length, why,
446
+ command: `dexterm autotrade limits set --scope ${scope} ${assign}`,
447
+ note: 'Nothing was changed: no tool can change a limit. Only the user applies this, on their own terminal.',
448
+ };
449
+ }
302
450
  // ───────────────────────────── backtest requests ─────────────────────────────
303
451
  requests() {
304
452
  const rs = new BacktestRequests(this.home()).list().slice(0, 20);