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
@@ -0,0 +1,134 @@
1
+ // dexterm — the AUTOTRADE screen's JOURNEY view (`Q`, then `J` on a strategy; 0.2.0 card C). One strategy's ladder:
2
+ // draft → backtested → forward-tested → always-on (paper) → real, with the last backtest (held-out split), the forward
3
+ // test (running or its result next to the backtest), the always-on service membership and the mode.
4
+ //
5
+ // Keys on the view: `b` backtests (recorded as evidence), `F` starts a 24 h forward test (paper, live data, the
6
+ // strategy's own bankroll, in the background), `X` stops it, `A` opens the pending ALWAYS-ON promotion's card (y
7
+ // applies it: autotrade.json gets the paper member). A promotion to REAL is never applied here: it needs the user's own
8
+ // caps and the typed phrase on a terminal (`dexterm autotrade promote apply <pp-id>`). Nothing here reaches a wallet,
9
+ // a key, a mandate or the signer.
10
+ import { dirname } from 'node:path';
11
+ import { PromotionFiles, applyAlwaysOn, createForwardTest, findForwardTest, forwardReport, journeyView, liveState, loadArmings, requestForwardStop, saveForwardTest, windowText, } from '../../../autotrade/src/index.js';
12
+ import { spawnForwardHost } from "../autotrade-cmd/forward.js";
13
+ export const defaultForwardSpawn = (home, t) => spawnForwardHost(home, t, process.env);
14
+ const homeOf = (ui) => dirname(ui.deps.store.dir);
15
+ export function openJourney(ui, id) {
16
+ if (!id)
17
+ return 'no strategy selected';
18
+ ui.journey = { id, view: null, error: null, forward: null, report: null, pending: [], readAt: 0 };
19
+ ui.view = 'journey';
20
+ refreshJourney(ui, true);
21
+ return ui.journey.error;
22
+ }
23
+ /** Re-read the journey (at most every 2 s unless forced): evidence, forward test, pending promotions. */
24
+ export function refreshJourney(ui, force = false) {
25
+ const j = ui.journey;
26
+ if (!j)
27
+ return;
28
+ const now = ui.deps.now();
29
+ if (!force && now - j.readAt < 2000)
30
+ return;
31
+ j.readAt = now;
32
+ const home = homeOf(ui);
33
+ const got = ui.deps.store.get(j.id);
34
+ if (!got || !got.ok) {
35
+ j.view = null;
36
+ j.error = got ? `${j.id} does not validate` : `no strategy ${j.id}`;
37
+ return;
38
+ }
39
+ try {
40
+ j.view = journeyView(home, got.strategy, { armedHash: loadArmings(home)[j.id]?.strategyHash ?? null });
41
+ j.forward = findForwardTest(home, j.id, now);
42
+ j.report = j.forward ? forwardReport(home, j.forward, now) : null;
43
+ j.pending = new PromotionFiles(home).list({ status: 'pending', id: j.id });
44
+ j.error = null;
45
+ }
46
+ catch (e) {
47
+ j.error = `journey unreadable: ${e.message}`;
48
+ }
49
+ }
50
+ /** `F`: a 24 h forward test of this version, on paper, live data, its own bankroll, started in the background. */
51
+ export function journeyForward(ui) {
52
+ const j = ui.journey;
53
+ if (!j)
54
+ return 'F starts a forward test from the journey view (J)';
55
+ const got = ui.deps.store.get(j.id);
56
+ if (!got || !got.ok)
57
+ return `no valid strategy ${j.id}`;
58
+ if (got.source !== 'user')
59
+ return `${j.id} is a preset: copy it first (d)`;
60
+ const home = homeOf(ui);
61
+ const c = createForwardTest(home, got.strategy, ui.deps.store.raw(j.id), { hours: 24, by: 'user:tui', now: ui.deps.now() });
62
+ if (!c.ok)
63
+ return c.why;
64
+ const pid = (ui.deps.forwardSpawn ?? defaultForwardSpawn)(home, c.test);
65
+ if (!pid) {
66
+ saveForwardTest(home, { ...c.test, state: 'failed', endedAt: ui.deps.now(), endReason: 'could not start the host' });
67
+ return `could not start it; in a terminal: dexterm autotrade forward start ${j.id}`;
68
+ }
69
+ refreshJourney(ui, true);
70
+ return `forward test ${c.test.forwardId} started · ${windowText(c.test.window)} · paper, live data, own bankroll $${c.test.bankrollUsd} · X stops it`;
71
+ }
72
+ /** `X`: stop the running forward test (it flattens and writes its report; an early stop does not count). */
73
+ export function journeyStopForward(ui) {
74
+ const j = ui.journey;
75
+ if (!j)
76
+ return 'X stops a forward test from the journey view (J)';
77
+ const t = findForwardTest(homeOf(ui), j.id, ui.deps.now());
78
+ if (!t)
79
+ return 'no forward test';
80
+ const r = requestForwardStop(homeOf(ui), t.forwardId, 'tui', ui.deps.now());
81
+ refreshJourney(ui, true);
82
+ return r.ok ? `${r.already ? 'already stopping' : 'stop requested'}: ${t.forwardId} flattens and writes its report (an early stop does not count)` : r.why;
83
+ }
84
+ /** `A`: open the pending always-on promotion's confirm card (y applies). Real is a terminal-only step. */
85
+ export function journeyPromote(ui) {
86
+ const j = ui.journey;
87
+ if (!j)
88
+ return 'A applies a promotion from the journey view (J)';
89
+ refreshJourney(ui, true);
90
+ const p = j.pending.find((x) => x.to === 'always-on');
91
+ if (p) {
92
+ ui.confirm = { kind: 'promote', proposalId: p.proposalId };
93
+ return `promotion ${p.proposalId} → always-on (proposed by ${p.actor}): y applies, any key cancels`;
94
+ }
95
+ const real = j.pending.find((x) => x.to === 'real');
96
+ if (real)
97
+ return `real mode is armed only on your own terminal with your caps and a typed phrase: dexterm autotrade promote apply ${real.proposalId}`;
98
+ return j.view?.next?.stage === 'always-on' ? `nothing pending: ask the analyst to propose it, or in a terminal: dexterm autotrade promote ${j.id} --to always-on` : 'no promotion pending';
99
+ }
100
+ /** The y / any-key answer on the promotion card. */
101
+ export function confirmPromote(ui, proposalId, yes) {
102
+ if (!yes)
103
+ return 'promotion not applied (it stays pending)';
104
+ const j = ui.journey;
105
+ const got = j ? ui.deps.store.get(j.id) : null;
106
+ if (!got || !got.ok)
107
+ return 'the strategy is not valid any more';
108
+ const a = applyAlwaysOn(homeOf(ui), proposalId, got.strategy, { by: 'user:tui', now: ui.deps.now() });
109
+ refreshJourney(ui, true);
110
+ return a.ok ? `applied: ${got.strategy.id} is an always-on paper member (${a.memberId}); the service runs it from its next start` : `not applied: ${a.why}`;
111
+ }
112
+ export function journeyLines(j, now) {
113
+ if (j.error || !j.view)
114
+ return [{ text: j.error ?? 'loading…', tone: 'warn' }];
115
+ const v = j.view;
116
+ const out = [
117
+ { text: `${v.id} v${v.version} · rules ${v.rulesHash.slice(0, 8)} · mode ${v.mode} · stage ${v.stage.toUpperCase()}`, tone: 'text' },
118
+ { text: '', tone: 'dim' },
119
+ ...v.rungs.map((r) => ({ text: `${r.ok ? '[x]' : '[ ]'} ${r.stage.padEnd(15)} ${r.detail}`, tone: (r.ok ? 'ok' : 'dim') })),
120
+ ...v.notes.map((n) => ({ text: `! ${n}`, tone: 'warn' })),
121
+ { text: v.next ? `next: ${v.next.stage} — ${v.next.how}` : 'top of the ladder: real (caps, kill switch, receipts: dexterm autotrade signal status)', tone: 'text' },
122
+ { text: '', tone: 'dim' },
123
+ ];
124
+ if (j.forward && j.report) {
125
+ const st = liveState(j.forward, now);
126
+ out.push({ text: `forward test ${j.forward.forwardId} · ${st.toUpperCase()} · ${windowText(j.forward.window)}`, tone: st === 'running' || st === 'starting' ? 'warn' : 'text' });
127
+ for (const l of j.report.lines.slice(1))
128
+ out.push({ text: l.replace(/^ /, ''), tone: 'text' });
129
+ out.push({ text: '', tone: 'dim' });
130
+ }
131
+ for (const p of j.pending)
132
+ out.push({ text: `pending promotion ${p.proposalId} → ${p.to} (proposed by ${p.actor})${p.to === 'always-on' ? ' · A to review' : ' · terminal only: dexterm autotrade promote apply'}`, tone: 'warn' });
133
+ return out;
134
+ }
@@ -4,6 +4,7 @@
4
4
  // strategy files; nothing here reaches a wallet, a key, a mandate or the signer.
5
5
  import { INPUT_MAX, backtestSubject, beginFieldEdit, beginImport, beginNew, commitInput, commitTyping, confirmKey, exportSelected, nextView, openEditor, reloadList, refreshRunner, revalidateTyping, runnerVerb, saveEditor, selectedEntry, startBacktest, startCompareBacktest, startSweep, toggleMark, undoEditor, } from "./autotrade-ui.js";
6
6
  import { openReview, reviewKey } from "./autotrade-proposals.js";
7
+ import { journeyForward, journeyPromote, journeyStopForward, openJourney, refreshJourney } from "./autotrade-journey.js";
7
8
  const PRINTABLE = (k) => (k.name === 'space' ? ' ' : k.name === 'char' && k.ch && !k.ctrl && !k.alt && !/[\u0000-\u001f\u007f]/.test(k.ch) ? k.ch : null);
8
9
  /** A captured key (input line, field value or confirm). Returns true when the key was consumed. */
9
10
  export function autotradeKey(ui, key) {
@@ -135,6 +136,8 @@ export function autotradeAction(ui, action) {
135
136
  case 'autotrade.save': return say(ui.view === 'editor' ? saveEditor(ui) : 's saves in the editor');
136
137
  case 'autotrade.undo': return say(ui.view === 'editor' ? undoEditor(ui) : 'u undoes in the editor');
137
138
  case 'autotrade.backtest': {
139
+ if (ui.view === 'journey' && ui.journey)
140
+ return say(startBacktest(ui, ui.journey.id));
138
141
  if (ui.view === 'compare')
139
142
  return say(startCompareBacktest(ui));
140
143
  if (ui.view === 'editor' && ui.editor)
@@ -150,6 +153,16 @@ export function autotradeAction(ui, action) {
150
153
  return say(id ? startSweep(ui, id) : 'no strategy selected');
151
154
  }
152
155
  case 'autotrade.proposal': return say(ui.view === 'list' ? openReview(ui) || null : 'o reviews chat proposals on the strategies list (esc)');
156
+ // task-1647: the journey view
157
+ case 'autotrade.journey':
158
+ if (ui.view === 'journey') {
159
+ refreshJourney(ui, true);
160
+ return say(null);
161
+ }
162
+ return say(openJourney(ui, ui.view === 'editor' ? ui.editor?.id : ui.view === 'backtest' ? backtestSubject(ui)?.id : e?.id));
163
+ case 'autotrade.forward': return say(ui.view === 'journey' ? journeyForward(ui) : 'F starts a forward test on the journey view (J)');
164
+ case 'autotrade.forwardstop': return say(ui.view === 'journey' ? journeyStopForward(ui) : 'X stops a forward test on the journey view (J)');
165
+ case 'autotrade.promote': return say(ui.view === 'journey' ? journeyPromote(ui) : 'A reviews a promotion on the journey view (J)');
153
166
  case 'autotrade.run':
154
167
  ui.view = 'runner';
155
168
  refreshRunner(ui);
@@ -5,6 +5,7 @@ import { Canvas, header, footer, breakpoint, fmtAgeSec, sparkline, wrap } from '
5
5
  import { compareRows as fleetCompareRows, compareTable as fleetCompareTable, fleetTotals } from '../../../autotrade/src/index.js';
6
6
  import { tokenize } from "../screener/filter.js";
7
7
  import { headerModel } from "../state.js";
8
+ import { journeyLines, refreshJourney } from "./autotrade-journey.js";
8
9
  import { AUTOTRADE_VIEWS, VIEW_TITLE, READ_ONLY_PATHS, backtestSubject, compareRows, getAt, heartbeatStale, issuesByField, screenHighlight, } from "./autotrade-ui.js";
9
10
  /** Per-screen memo of file reads + validation, keyed by what the list says about a strategy (id, hash, version,
10
11
  * last backtest): a frame never re-reads a strategy file; a save, import or backtest reloads the list and so the key. */
@@ -46,10 +47,14 @@ export function renderAutotrade(c, state) {
46
47
  renderCompare(c, ui, area);
47
48
  else if (ui.view === 'backtest')
48
49
  renderBacktest(c, ui, area, now);
50
+ else if (ui.view === 'journey')
51
+ renderJourneyView(c, ui, area, now);
49
52
  else
50
53
  renderRunner(c, ui, area, now);
51
- if (ui.confirm)
54
+ if (ui.confirm?.kind === 'flatten')
52
55
  confirmCard(c);
56
+ else if (ui.confirm?.kind === 'promote')
57
+ promoteCard(c, ui, ui.confirm.proposalId);
53
58
  if (ui.review)
54
59
  reviewCard(c, ui);
55
60
  footer(c, hintsFor(ui), t.name);
@@ -68,6 +73,8 @@ function tabs(c, ui) {
68
73
  const off = (v === 'compare' && !ui.compare) || (v === 'editor' && !ui.editor);
69
74
  x = c.text(x, y, ` ${VIEW_TITLE[v]} `, { fg: on ? t.text : off ? t.dim : t.muted, bg: on ? t.tabActiveBg : t.bg, b: on }) + 1;
70
75
  }
76
+ if (ui.view === 'journey')
77
+ x = c.text(x, y, ` ${VIEW_TITLE.journey} `, { fg: t.text, bg: t.tabActiveBg, b: true }) + 1;
71
78
  const line = ui.input
72
79
  ? `${ui.input.kind === 'import' ? 'import file: ' : ui.input.kind === 'dup' ? `duplicate ${ui.input.from} as: ` : `new from ${ui.input.from} as: `}${ui.input.text}█ ⏎ ok · esc cancel`
73
80
  : ui.notice;
@@ -77,7 +84,7 @@ function tabs(c, ui) {
77
84
  function hintsFor(ui) {
78
85
  const common = [['esc', 'back'], ['q', 'quit']];
79
86
  if (ui.confirm)
80
- return [['y', 'flatten'], ['any key', 'cancel']];
87
+ return [['y', ui.confirm.kind === 'promote' ? 'apply promotion' : 'flatten'], ['any key', 'cancel']];
81
88
  if (ui.review)
82
89
  return ui.review.stage === 'less-safe' ? [['y', 'apply (less safe)'], ['any key', 'cancel']] : [['y', ui.review.check.ok && ui.review.check.riskDelta === 'looser' ? 'apply… (less safe)' : 'apply'], ['n', 'dismiss'], ['any key', 'close']];
83
90
  if (ui.input)
@@ -89,6 +96,7 @@ function hintsFor(ui) {
89
96
  : [['tab', 'view'], ['↑↓', 'field'], ['⏎', 'change'], ['s', 'save'], ['u', 'undo'], ['b', 'backtest'], ['w', 'sweep'], ...common];
90
97
  case 'compare': return [['tab', 'view'], ['↑↓', 'scroll'], ['b', 'backtest both, one tape'], ...common];
91
98
  case 'backtest': return [['tab', 'view'], ['b', 'run again'], ['w', 'sweep (held-out)'], ...common];
99
+ case 'journey': return [['b', 'backtest'], ['F', 'forward test'], ['X', 'stop it'], ['A', 'promotion'], ['J', 'refresh'], ...common];
92
100
  default: return [['tab', 'view'], ['!', 'kill'], ['p', 'pause entries'], ['f', 'flatten (confirm)'], ...common];
93
101
  }
94
102
  }
@@ -206,7 +214,7 @@ function renderList(c, ui, a, now) {
206
214
  }
207
215
  const running = (ui) => ui.runner.kind === 'ok';
208
216
  /** task-1528: the running runner's mode from its rendezvous ('live' = mainnet through the live signer). */
209
- const runMode = (ui) => { const m = ui.runner.kind === 'ok' ? ui.runner.snap.mode : 'paper'; return m === 'live' || m === 'devnet' ? m : 'paper'; };
217
+ const runMode = (ui) => { const m = ui.runner.kind === 'ok' ? ui.runner.snap.mode : 'paper'; return m === 'live' || m === 'devnet' || m === 'real' ? m : 'paper'; };
210
218
  /** One chat proposal card (§2a): id, hashes, the diff lines, validation, LOOSER in the warn colour. */
211
219
  export function proposalCard(c, p, x, y, w, maxY) {
212
220
  const t = c.theme;
@@ -639,9 +647,9 @@ function sweepRow(x, pw) {
639
647
  function renderRunner(c, ui, a, now) {
640
648
  const t = c.theme;
641
649
  const rr = ui.runner;
642
- const sh = 5;
643
650
  const mode = runMode(ui);
644
- const sbox = c.box(a.x, a.y, a.w, sh, { title: `RUNNER · ${mode === 'live' ? 'LIVE · MAINNET' : mode === 'devnet' ? 'DEVNET (live signer rehearsal)' : 'PAPER'}`, caption: `status file: ${rr.path || '~/.dexterm/autotrade/runner.json'}`, focus: true });
651
+ const sh = mode === 'real' ? (ui.runner.kind === 'ok' && ui.runner.snap.real?.limits ? 8 : 6) : 5;
652
+ const sbox = c.box(a.x, a.y, a.w, sh, { title: `RUNNER · ${mode === 'live' ? 'LIVE · MAINNET' : mode === 'devnet' ? 'DEVNET (live signer rehearsal)' : mode === 'real' ? 'REAL · LIVE TRADE FIRE signals to your executor (no key here)' : 'PAPER'}`, caption: `status file: ${rr.path || '~/.dexterm/autotrade/runner.json'}`, focus: true });
645
653
  let y = sbox.y;
646
654
  const sl = (s, fg = t.text, b = false) => { if (y < sbox.y + sbox.h) {
647
655
  c.text(sbox.x + 1, y, fit(s, sbox.w - 2), { fg, bg: sbox.bg, b });
@@ -665,6 +673,23 @@ function renderRunner(c, ui, a, now) {
665
673
  sl(`${stale ? 'STALE' : s.state.toUpperCase()} · ${s.strategy.id} v${s.strategy.version} (${s.strategy.hash}) · pid ${s.pid} · heartbeat ${age} ago${stale ? ' (no heartbeat for over 30 s: treat it as down)' : ''}`, stateFg, true);
666
674
  sl(`book $${s.book.equityUsd.toFixed(2)} of $${s.book.startUsd.toFixed(2)} · cash $${s.book.cashUsd.toFixed(2)} · realized today ${usd(s.book.realizedTodayUsd)} · model spend $${s.spend.todayUsd.toFixed(2)} of $${s.spend.dayCapUsd.toFixed(2)} today`);
667
675
  sl(s.haltReason ? `halted: ${s.haltReason}` : 'no halt', s.haltReason ? t.down : t.dim, !!s.haltReason);
676
+ // task-1645: real mode: the account, the kill switch, the outbox, each channel's health, unconfirmed positions.
677
+ const rm = s.real;
678
+ if (rm) {
679
+ const chans = rm.channels.map((h) => `${h.name} ${h.failing ? `FAILING ×${h.failing}` : 'ok'}${h.pending ? ` (${h.pending} pending)` : ''}`).join(', ') || 'none';
680
+ const unconf = rm.positions.filter((p) => p.state === 'unconfirmed').length;
681
+ sl(`real · account ${rm.accountId} · ${rm.killed ? `KILL ON (${rm.killed})` : 'kill off'} · outbox ${rm.outboxDepth} pending · ${chans} · ${unconf} unconfirmed · today ${rm.day.buySol} of ${rm.caps.dailySol ?? 'off'} SOL`, rm.killed || rm.channels.some((h) => h.failing) || unconf ? t.warn : t.text, true);
682
+ // task-1654: the limits panel (status only: limits change on your own terminal, `dexterm autotrade limits`).
683
+ if (rm.limits) {
684
+ const L = rm.limits;
685
+ const v = (k, unit) => (typeof L[k] === 'number' ? `${L[k]}${unit}` : 'off');
686
+ const pause = rm.risk?.buyPause;
687
+ sl(`limits · ${v('perTradeSol', ' SOL')}/trade${typeof L.perTradePct === 'number' ? ` (${L.perTradePct}% of book)` : ''} · ${v('dailySol', ' SOL')}/day · ${v('buysPerDay', '')} buys · ${v('maxOpenPositions', '')} open · loss stop ${v('dailyLossSol', ' SOL')} · streak ${rm.risk?.lossStreak ?? 0}/${v('lossStreak', '')}${rm.risk?.breaker ? ' · BREAKER TRIPPED' : ''}${pause ? ` · BUYS PAUSED: ${pause}` : ''} · dexterm autotrade limits show`, pause || rm.risk?.breaker ? t.warn : t.muted, !!pause);
688
+ const ex = rm.executor;
689
+ if (ex)
690
+ sl(`executor · ${ex.alive === false ? 'SILENT' : ex.alive ? 'alive' : 'not judged'}${ex.silentSec !== null && ex.lastSeenBy !== 'run-start' ? ` · last ${ex.lastSeenBy} ${fmtAgeSec(ex.silentSec)} ago` : ''}${ex.heartbeat ? ` · ${ex.heartbeat.executor}${ex.heartbeat.dryRun ? ' (dry run)' : ''}` : ' · no heartbeat yet'}${ex.lastReceipt ? ` · receipt #${ex.lastReceipt.nonce} ${ex.lastReceipt.status}` : ''}${ex.receiptLagSec !== null ? ` · lag ${ex.receiptLagSec} s` : ''}${ex.windowMin !== null ? ` · pauses buys after ${ex.windowMin} min silent` : ''}`, ex.alive === false ? t.down : t.muted, ex.alive === false);
691
+ }
692
+ }
668
693
  // task-1529: a `run --all` shows its strategies side by side above the shared panels (rows tagged [strategy]).
669
694
  const fleet = ui.fleet;
670
695
  let fh = 0;
@@ -703,7 +728,7 @@ function renderRunner(c, ui, a, now) {
703
728
  [` last: ${k.lastDecision ?? '—'}`, t.muted],
704
729
  ]));
705
730
  panel(a.x + 2 * third, a.w - 2 * third, `POSITIONS · ${s.positions.length}`, s.positions.flatMap((p) => [
706
- [`${tag(p)}${p.symbol.padEnd(8)}${pct(p.pnlPct).padStart(8)}${usd(p.pnlUsd).padStart(9)}${p.freeBag ? ' free bag' : ''}`, p.pnlUsd >= 0 ? t.up : t.down],
731
+ [`${tag(p)}${p.symbol.padEnd(8)}${pct(p.pnlPct).padStart(8)}${usd(p.pnlUsd).padStart(9)}${p.freeBag ? ' free bag' : ''}${p.confirm ? ` · ${p.confirm}` : ''}`, p.pnlUsd >= 0 ? t.up : t.down],
707
732
  [` ${p.profile} · next: ${p.next ?? '—'}`, t.muted],
708
733
  ]));
709
734
  const lb = c.box(a.x, a.y + sh + fh + upper, a.w, ledgerH, { title: 'RUN LEDGER · newest last', caption: 'hash-chained; every step with its exact inputs' });
@@ -723,3 +748,29 @@ function confirmCard(c) {
723
748
  c.text(b.x + 1, b.y + 1, fit('New entries stop first; mechanical exits keep running.', b.w - 2), { fg: t.muted, bg: b.bg });
724
749
  c.text(b.x + 1, b.y + 3, 'y flatten · any other key cancels', { fg: t.warn, bg: b.bg, b: true });
725
750
  }
751
+ // ───────────────────────────── journey (task-1647) ─────────────────────────────
752
+ function renderJourneyView(c, ui, a, now) {
753
+ const t = c.theme;
754
+ refreshJourney(ui);
755
+ const j = ui.journey;
756
+ const box = c.box(a.x, a.y, a.w, a.h, { title: `JOURNEY · ${j?.id ?? '—'}`, caption: 'draft → backtested → forward-tested → always-on (paper) → real · one rung at a time, on these exact rules', focus: true });
757
+ const bg = box.bg;
758
+ if (!j) {
759
+ c.text(box.x + 1, box.y, 'J on a strategy opens its journey', { fg: t.dim, bg });
760
+ return;
761
+ }
762
+ const tone = { text: t.text, ok: t.up, warn: t.warn, dim: t.dim };
763
+ // tables keep their columns: only a line wider than the box wraps
764
+ const lines = journeyLines(j, now).flatMap((l) => (l.text.length <= box.w - 2 ? [l] : wrap(l.text, box.w - 2).map((x) => ({ text: x, tone: l.tone }))));
765
+ lines.slice(0, box.h).forEach((l, i) => c.text(box.x + 1, box.y + i, fit(l.text, box.w - 2), { fg: tone[l.tone], bg }));
766
+ }
767
+ function promoteCard(c, ui, proposalId) {
768
+ const t = c.theme;
769
+ const p = ui.journey?.pending.find((x) => x.proposalId === proposalId);
770
+ const body = p ? p.card : [`promotion ${proposalId}`];
771
+ const w = Math.min(c.w - 4, 104), h = Math.min(c.h - 4, body.length + 5);
772
+ const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
773
+ const box = c.box(x, y, w, h, { title: 'APPLY PROMOTION? (paper: the always-on service)', focus: true });
774
+ body.slice(0, h - 4).forEach((l, i) => c.text(box.x + 1, box.y + i, fit(l, box.w - 2), { fg: t.text, bg: box.bg }));
775
+ c.text(box.x + 1, box.y + h - 3, fit(`proposed by ${p?.actor ?? '?'} · y applies (autotrade.json gets the paper member) · any other key cancels`, box.w - 2), { fg: t.warn, bg: box.bg, b: true });
776
+ }
@@ -15,13 +15,14 @@
15
15
  // cards; applying them is the chat card's keypress flow). Kill / pause / flatten go through RunnerControl: the
16
16
  // runner's control file (task-1509 fileControl), the same request `dexterm autotrade stop|flatten` writes.
17
17
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
- import { join } from 'node:path';
18
+ import { dirname, join } from 'node:path';
19
19
  import { applyOps, dextermHome } from '../../../custom/src/index.js';
20
- import { StrategyStore, MAX_OPS, STRATEGY_ID, activeRunner, bumpPatch, fmtIssue, readControl, readFleetStatus, requestControl, runnerRendezvousPath, validateStrategy, } from '../../../autotrade/src/index.js';
20
+ import { StrategyStore, MAX_OPS, recordBacktestEvidence, STRATEGY_ID, activeRunner, bumpPatch, fmtIssue, readControl, readFleetStatus, requestControl, runnerRendezvousPath, validateStrategy, } from '../../../autotrade/src/index.js';
21
21
  import { parseFilter } from "../screener/filter.js";
22
22
  import { proposalFilesFor, proposalSource } from "./autotrade-proposals.js";
23
+ import { confirmPromote } from "./autotrade-journey.js";
23
24
  export const AUTOTRADE_VIEWS = ['list', 'editor', 'compare', 'backtest', 'runner'];
24
- export const VIEW_TITLE = { list: 'STRATEGIES', editor: 'EDITOR', compare: 'COMPARE', backtest: 'BACKTEST', runner: 'RUNNER' };
25
+ export const VIEW_TITLE = { list: 'STRATEGIES', editor: 'EDITOR', compare: 'COMPARE', backtest: 'BACKTEST', runner: 'RUNNER', journey: 'JOURNEY' };
25
26
  export const SECTIONS = ['entry', 'exits', 'risk', 'sizing', 'agent', 'execution'];
26
27
  /** Who the journal names for an edit made on this screen (the same actor the CLI uses for the user's own edits). */
27
28
  export const TUI_ACTOR = 'user';
@@ -94,9 +95,9 @@ function runnerShapeError(d) {
94
95
  const o = d;
95
96
  if (o.schema !== RUNNER_SCHEMA)
96
97
  return `unknown rendezvous shape ${JSON.stringify(o.schema)} (expected ${RUNNER_SCHEMA})`;
97
- // Paper only: a rendezvous that claims anything else is shown as an error, never as a running live runner.
98
- if (o.mode !== 'paper')
99
- return `mode ${JSON.stringify(o.mode)}: this screen shows paper runners only`;
98
+ // Paper, or real (task-1645: signals to the user's executor). Anything else is an error, never a running live runner.
99
+ if (o.mode !== 'paper' && o.mode !== 'real')
100
+ return `mode ${JSON.stringify(o.mode)}: this screen shows paper runners only (and real-mode signal runners)`;
100
101
  for (const k of ['pid', 'startedAt', 'heartbeatAt'])
101
102
  if (typeof o[k] !== 'number')
102
103
  return `${k} is not a number`;
@@ -133,7 +134,7 @@ export function defaultDeps(env = process.env, jobs = null) {
133
134
  export function newAutotradeUi(deps) {
134
135
  const ui = {
135
136
  view: 'list', deps, entries: [], selected: 0, marked: [], notice: null, input: null, editor: null, compare: null,
136
- focusId: null, backtests: new Map(), compareRun: null, sweep: null, runner: { kind: 'none', path: '' }, fleet: null, runnerReadAt: 0, confirm: null, review: null,
137
+ focusId: null, backtests: new Map(), compareRun: null, sweep: null, runner: { kind: 'none', path: '' }, fleet: null, runnerReadAt: 0, confirm: null, review: null, journey: null,
137
138
  };
138
139
  reloadList(ui);
139
140
  refreshRunner(ui);
@@ -525,6 +526,13 @@ export function startBacktest(ui, id) {
525
526
  job.result = out;
526
527
  job.doneAt = ui.deps.now();
527
528
  recordLine(ui, s, out.report);
529
+ // task-1647: the held-out split of a stored strategy is its journey evidence
530
+ if (out.evidence && ui.deps.store.get(s.id)?.ok) {
531
+ try {
532
+ recordBacktestEvidence(dirname(ui.deps.store.dir), out.evidence);
533
+ }
534
+ catch { /* evidence is best effort here */ }
535
+ }
528
536
  }, (e) => { job.status = 'error'; job.error = errText(e); job.doneAt = ui.deps.now(); });
529
537
  return `backtesting ${s.id} v${s.version} on the last ${BACKTEST_DAYS} days of recorded history…`;
530
538
  }
@@ -631,6 +639,8 @@ export function confirmKey(ui, yes) {
631
639
  ui.confirm = null;
632
640
  if (!c)
633
641
  return '';
642
+ if (c.kind === 'promote')
643
+ return confirmPromote(ui, c.proposalId, yes);
634
644
  return yes ? ui.deps.control.flatten() : 'flatten cancelled';
635
645
  }
636
646
  /** The views tab cycles through (compare only once two strategies were marked). */
@@ -7,7 +7,7 @@ import { existsSync } from 'node:fs';
7
7
  import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
8
8
  import { defaultDbPath } from '../../../data/src/index.js';
9
9
  import { openHistoryReadOnly } from '../../../recorder/src/index.js';
10
- import { backtest, sweep, tapeFromDb, tapeFromDoc } from '../../../autotrade/src/index.js';
10
+ import { backtest, backtestEvidence, sweep, tapeFromDb, tapeFromDoc } from '../../../autotrade/src/index.js';
11
11
  const DAY_MS = 86_400_000;
12
12
  /** The equity curve is resampled to this many points before it crosses the thread boundary (it feeds a sparkline). */
13
13
  const CURVE_POINTS = 240;
@@ -42,7 +42,9 @@ export async function backtestOn(tape, ss) {
42
42
  const out = [];
43
43
  for (const s of ss) {
44
44
  const { report, run } = await backtest(s, tape);
45
- out.push({ report, curve: resample(run.equityCurve.map((p) => p.equity), CURVE_POINTS) });
45
+ // task-1647: one strategy alone (not a compare) also runs on each half of the held-out split: journey evidence
46
+ const evidence = ss.length === 1 && s.mode === 'paper' ? (await backtestEvidence(s, tape, { by: 'user:tui', full: { report, run } })).evidence : undefined;
47
+ out.push({ report, curve: resample(run.equityCurve.map((p) => p.equity), CURVE_POINTS), ...(evidence ? { evidence } : {}) });
46
48
  }
47
49
  return out;
48
50
  }
@@ -3,8 +3,13 @@
3
3
  // new launches / runners / pace / "what is working" score. No model in the hot path; the board carries
4
4
  // its own limits (name-based, X/TG-only narratives missed, honest unclassified share). w flips 1h/24h,
5
5
  // M opens the HUD popup. The board is computed in state.ts (computeNarrativeView) on entry and the tick.
6
+ // task-1818: `n` flips to the EXPERIMENTAL heat table (@dexterm/narratives: clustered by what the name says,
7
+ // lifecycle + traction score per narrative); ⏎ drills into one narrative's member coins, esc returns to the table.
6
8
  import { Canvas, header, footer, badge, breakpoint } from '../../../tui/src/index.js';
7
9
  import { headerModel } from "../state.js";
10
+ import { clean } from '../../../packs/src/index.js';
11
+ import { HEAT_NOTE } from '../../../narratives/src/index.js';
12
+ import { heatRows, drillDetail, currentTracker, narrativesSource, UNTRUSTED_NOTE } from "../narratives.js";
8
13
  const fmtUsd = (v) => { if (v == null)
9
14
  return '—'; const a = Math.abs(v); if (a >= 1e9)
10
15
  return (v / 1e9).toFixed(2) + 'B'; if (a >= 1e6)
@@ -12,6 +17,13 @@ const fmtUsd = (v) => { if (v == null)
12
17
  return (v / 1e3).toFixed(1) + 'K'; return v.toFixed(0); };
13
18
  const fmtPct = (v) => (v == null ? '—' : (v > 0 ? '+' : '') + v.toFixed(1) + '%');
14
19
  export function renderNarrative(c, state) {
20
+ if (state.narrativesUi?.mode === 'heat') {
21
+ if (state.narrativesUi.drill)
22
+ renderDrill(c, state);
23
+ else
24
+ renderHeat(c, state);
25
+ return;
26
+ }
15
27
  const t = c.theme;
16
28
  const bp = breakpoint(c.w);
17
29
  header(c, headerModel(state));
@@ -27,7 +39,7 @@ export function renderNarrative(c, state) {
27
39
  if (!board || board.narratives.every((n) => n.tokens === 0)) {
28
40
  c.textC(box.x, box.y + 1, box.w, 'collecting narratives — run the app with the recorder on, or open the screener first', { fg: t.muted, bg, i: true });
29
41
  c.textC(box.x, box.y + 3, box.w, view?.error ? `(${view.error})` : 'clusters come from token names against a lexicon (owner: maintainers); X/Telegram-only narratives are missed', { fg: t.dim, bg });
30
- footer(c, [['w', '1h/24h'], ['esc', 'back'], ['q', 'quit']], t.name);
42
+ footer(c, [['w', '1h/24h'], ['n', 'narratives (exp)'], ['esc', 'back'], ['q', 'quit']], t.name);
31
43
  return;
32
44
  }
33
45
  const shown = board.narratives.filter((n) => n.tokens > 0);
@@ -86,5 +98,145 @@ export function renderNarrative(c, state) {
86
98
  const ly = box.y + box.h - 1;
87
99
  if (wide)
88
100
  c.textC(box.x, ly, box.w, `model output: none (deterministic lexicon v${board.meta.lexicon_version}) · ${board.meta.copycat_tokens} copycat symbols · name-based · pace '*' = all tokens · compute ${board.meta.compute_ms}ms`, { fg: t.dim, bg });
89
- footer(c, [['↑↓', 'move'], ['w', win === '1h' ? '→24h' : '→1h'], ['M', 'popup'], ['esc', 'back'], ['q', 'quit']], t.name);
101
+ footer(c, [['↑↓', 'move'], ['w', win === '1h' ? '→24h' : '→1h'], ['n', 'narratives (exp)'], ['M', 'popup'], ['esc', 'back'], ['q', 'quit']], t.name);
102
+ }
103
+ // ---- task-1818 EXPERIMENTAL heat table + drill-down ------------------------------------------------------------
104
+ const heatUsd = (v) => (v == null ? '—' : '$' + fmtUsd(v));
105
+ const fmtShare = (v) => (v == null ? '—' : (v * 100).toFixed(1) + '%');
106
+ const fmtX = (v) => (v == null || !Number.isFinite(v) ? '—' : v.toFixed(2) + 'x');
107
+ function lcColor(t, lc) {
108
+ // states are measured heat, so none is painted as a market up/down colour (no green "go")
109
+ return lc === 'ignition' ? t.accent : lc === 'peak' ? t.text : lc === 'fading' ? t.warn : lc === 'dead' ? t.dim : t.muted;
110
+ }
111
+ /** The limits line: the full HEAT_NOTE when it fits, else the short form (the note must never be the part that is cut). */
112
+ function limitsLine(w, extra) {
113
+ const full = `${HEAT_NOTE} · ${UNTRUSTED_NOTE} · ${extra}`;
114
+ return full.length <= w ? full : `measured heat, not advice · ${UNTRUSTED_NOTE} · ${extra}`;
115
+ }
116
+ /** The status line under an empty heat table: building (with the one-time stall warning), failed, or nothing yet. */
117
+ function heatPlaceholder(state) {
118
+ const ui = state.narrativesUi;
119
+ const rec = narrativesSource(state) === 'recorded';
120
+ if (ui?.error)
121
+ return [`narratives failed: ${clean(ui.error, 120)}`, 'the theme board (v) still works'];
122
+ if (!currentTracker(state))
123
+ return [rec ? 'building narratives from recorded history…' : 'building narratives from the visible rows…',
124
+ rec ? 'first build reads the last 26 h of the recorder: tens of seconds, the screen stays live (once per run)' : 'no recorder DB here (fixtures / replay): no history, so every narrative starts as a spark'];
125
+ return ['no narrative with 2+ coins yet', 'a narrative needs at least two coins whose names share a theme · measured heat, not advice'];
126
+ }
127
+ function renderHeat(c, state) {
128
+ const t = c.theme;
129
+ const bp = breakpoint(c.w);
130
+ header(c, headerModel(state));
131
+ const ui = state.narrativesUi;
132
+ const rows = heatRows(state);
133
+ const tr = currentTracker(state);
134
+ const src = narrativesSource(state) === 'recorded' ? 'recorded history' : 'visible rows';
135
+ const top = 2, bottom = c.h - 1;
136
+ const title = `NARRATIVES · EXPERIMENTAL · ${tr ? `${rows.length} narratives · ${tr.coins.size} coins` : 'building'} · ${src}${ui.busy && tr ? ' · refreshing' : ''}`;
137
+ const caption = bp === 'compact' ? undefined : 'measured heat, not advice';
138
+ const box = c.box(0, top, c.w, bottom - top, { title, caption, focus: true });
139
+ const bg = box.bg;
140
+ const keys = [['↑↓', 'move'], ['⏎', 'drill'], ['n', 'theme board'], ['esc', 'back'], ['q', 'quit']];
141
+ if (!rows.length) {
142
+ const [a, b] = heatPlaceholder(state);
143
+ c.textC(box.x, box.y + 1, box.w, a, { fg: t.muted, bg, i: true });
144
+ c.textC(box.x, box.y + 3, box.w, b, { fg: t.dim, bg });
145
+ c.textC(box.x, box.y + 5, box.w, HEAT_NOTE, { fg: t.dim, bg });
146
+ footer(c, keys, t.name);
147
+ return;
148
+ }
149
+ const sel = Math.max(0, Math.min(rows.length - 1, ui.selected));
150
+ // columns: [x, header]; a column that does not fit the box is dropped (the runner first)
151
+ const cols = [[1, '#', 3], [5, 'NARRATIVE', 18], [24, 'THEME', 12], [37, 'STATE', 10], [48, 'COINS', 5], [54, 'NEW/h', 5], [60, 'VOL 1h', 9], [70, 'SHARE', 6], [77, 'TRADERS', 7], [85, 'SCORE', 5], [91, 'TOP RUNNER (x · drawdown)', 28]];
152
+ const fits = (i) => cols[i][0] + Math.min(cols[i][2], 12) <= box.w - 1;
153
+ let cy = box.y;
154
+ cols.forEach(([x, h], i) => { if (fits(i))
155
+ c.text(box.x + x, cy, h, { fg: t.dim, bg }, box.w - x - 1); });
156
+ cy += 1;
157
+ c.hline(box.x, cy, box.w, { fg: t.border, bg });
158
+ cy += 1;
159
+ const scroll = Math.max(0, sel - (box.h - 6));
160
+ for (let i = scroll; i < rows.length; i++) {
161
+ if (cy >= box.y + box.h - 2)
162
+ break;
163
+ const s = rows[i];
164
+ const w1 = s.windows['1h'];
165
+ const selected = i === sel;
166
+ const rb = selected ? t.selBg : bg;
167
+ c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
168
+ if (selected)
169
+ c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
170
+ const cell = (k, v, st) => { if (fits(k))
171
+ c.text(box.x + cols[k][0], cy, v, { ...st, bg: rb }, Math.min(cols[k][2], box.w - cols[k][0] - 1)); };
172
+ cell(0, s.rank == null ? '·' : String(s.rank), { fg: s.rank == null ? t.dim : t.accent, b: true });
173
+ cell(1, clean(s.label, 18), { fg: selected ? t.borderFocus : t.text, b: s.rank != null });
174
+ cell(2, s.theme ? clean(s.theme, 12) : '—', { fg: t.muted });
175
+ cell(3, s.lifecycle + (s.saturated ? '*' : ''), { fg: lcColor(t, s.lifecycle), b: s.lifecycle === 'ignition' });
176
+ cell(4, String(s.members), { fg: t.text });
177
+ cell(5, w1.newPerHour.toFixed(0), { fg: t.muted });
178
+ cell(6, heatUsd(w1.volume), { fg: t.text });
179
+ cell(7, fmtShare(w1.volumeShare), { fg: t.muted });
180
+ cell(8, String(w1.traders), { fg: t.muted });
181
+ cell(9, String(s.score), { fg: t.accent, b: true });
182
+ const r = s.topRunner;
183
+ cell(10, r ? `${clean(r.symbol, 10)} ${fmtX(r.x)} · ${r.drawdownPct.toFixed(0)}%` : '—', { fg: r ? t.text : t.dim });
184
+ cy += 1;
185
+ }
186
+ c.textC(box.x, box.y + box.h - 1, box.w, limitsLine(box.w, '* = copy-cat saturated'), { fg: t.dim, bg });
187
+ footer(c, keys, t.name);
188
+ }
189
+ function renderDrill(c, state) {
190
+ const t = c.theme;
191
+ header(c, headerModel(state));
192
+ const ui = state.narrativesUi;
193
+ const d = drillDetail(state);
194
+ const top = 2, bottom = c.h - 1;
195
+ const keys = [['↑↓', 'move'], ['⏎', 'open coin'], ['esc', 'table'], ['q', 'quit']];
196
+ if (!d) {
197
+ const box = c.box(0, top, c.w, bottom - top, { title: 'NARRATIVE · EXPERIMENTAL', focus: true });
198
+ c.textC(box.x, box.y + 1, box.w, 'this narrative is no longer tracked (esc returns to the table)', { fg: t.muted, bg: box.bg, i: true });
199
+ footer(c, keys, t.name);
200
+ return;
201
+ }
202
+ const w1 = d.windows['1h'];
203
+ const title = `NARRATIVE · ${clean(d.label, 24)} · ${d.lifecycle}${d.saturated ? ' · copy-cat saturated' : ''} · ${d.memberList.length} coin${d.memberList.length === 1 ? '' : 's'} · EXPERIMENTAL`;
204
+ const box = c.box(0, top, c.w, bottom - top, { title, caption: 'measured heat, not advice · members in spawn order', focus: true });
205
+ const bg = box.bg;
206
+ let cy = box.y;
207
+ c.text(box.x + 1, cy, `lifecycle rule (${d.lifecycle}): ${d.lifecycleRule}`, { fg: lcColor(t, d.lifecycle), bg }, box.w - 2);
208
+ cy += 1;
209
+ c.text(box.x + 1, cy, `1h: vol ${heatUsd(w1.volume)} · share ${fmtShare(w1.volumeShare)} · new/h ${w1.newPerHour.toFixed(0)} · traders ${w1.traders} · score ${d.score}${d.rank ? ` · rank ${d.rank}` : ''}${d.theme ? ` · theme ${clean(d.theme, 16)}` : ''}`, { fg: t.muted, bg }, box.w - 2);
210
+ cy += 1;
211
+ const cols = [[1, '#', 4], [6, 'SYMBOL', 12], [19, 'NAME', 26], [46, 'X SINCE 1ST', 11], [58, 'LIQ', 9], [68, 'VOL 1h', 9]];
212
+ const fits = (i) => cols[i][0] + Math.min(cols[i][2], 8) <= box.w - 1;
213
+ cols.forEach(([x, h], i) => { if (fits(i))
214
+ c.text(box.x + x, cy, h, { fg: t.dim, bg }); });
215
+ cy += 1;
216
+ c.hline(box.x, cy, box.w, { fg: t.border, bg });
217
+ cy += 1;
218
+ const n = d.memberList.length;
219
+ const sel = Math.max(0, Math.min(n - 1, ui.drill.selected));
220
+ const scroll = Math.max(0, sel - (box.h - 8));
221
+ for (let i = scroll; i < n; i++) {
222
+ if (cy >= box.y + box.h - 1)
223
+ break;
224
+ const m = d.memberList[i];
225
+ const selected = i === sel;
226
+ const rb = selected ? t.selBg : bg;
227
+ c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
228
+ if (selected)
229
+ c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
230
+ const cell = (k, v, fg) => { if (fits(k))
231
+ c.text(box.x + cols[k][0], cy, v, { fg, bg: rb }, Math.min(cols[k][2], box.w - cols[k][0] - 1)); };
232
+ cell(0, `#${m.spawn}`, t.dim);
233
+ cell(1, clean(m.symbol, 12), selected ? t.borderFocus : t.text);
234
+ cell(2, clean(m.name, 26), t.muted);
235
+ cell(3, fmtX(m.x), m.x == null ? t.dim : m.x >= 1 ? t.up : t.down);
236
+ cell(4, heatUsd(m.liq), t.text);
237
+ cell(5, heatUsd(m.vol1h), t.text);
238
+ cy += 1;
239
+ }
240
+ c.textC(box.x, box.y + box.h - 1, box.w, limitsLine(box.w, 'x = now vs first $10K+ pool price'), { fg: t.dim, bg });
241
+ footer(c, keys, t.name);
90
242
  }
@@ -301,6 +301,7 @@ export function makeFixtureState(opts = {}) {
301
301
  ai: null,
302
302
  analystByMint: new Map(),
303
303
  larpByMint: new Map(),
304
+ classifierByMint: new Map(),
304
305
  colorDepth: opts.colorDepth ?? 'truecolor',
305
306
  };
306
307
  if (opts.priceMode)
@@ -15,7 +15,7 @@
15
15
  // The runner starts the bot by itself when it is set up and paired (`dexterm autotrade run … --paper`; --no-telegram
16
16
  // keeps it off). Nothing here is hosted: the bot long-polls from this machine.
17
17
  import { dextermHome } from '../../custom/src/index.js';
18
- import { ChatAudit, PairingWindow, TelegramConnector, TIERS_ALL, chooseSecretStore, connectorsPath, defaultTelegram, loadConnectors, looksLikeBotToken, redact, saveConnectors, secretStore, verifyChatAudit, ChatHub, } from '../../autotrade/src/index.js';
18
+ import { ChatAudit, PairingWindow, TelegramConnector, TIERS_ALL, chooseSecretStore, connectorsPath, defaultTelegram, loadConnectors, looksLikeBotToken, redact, saveConnectors, secretStore, verifyChatAudit, ChatHub, enabledChannels, parseSignalsConfig, } from '../../autotrade/src/index.js';
19
19
  export const TELEGRAM_USAGE = `dexterm telegram: your own Telegram bot for the autotrade runner (paper first). Nothing is hosted.
20
20
 
21
21
  dexterm telegram setup [--tiers notify,control,approve] [--no-pair]
@@ -321,6 +321,8 @@ export async function telegramCommand(argv, io, deps = {}) {
321
321
  paired: t.allow.map((a) => ({ senderId: a.senderId, chatId: a.chatId, tiers: a.tiers, pairedAt: a.pairedAt ? new Date(a.pairedAt).toISOString() : null })),
322
322
  approvals: t.approvals, approvalExpirySec: t.approvalExpirySec, repriceTolPct: t.repriceTolPct, notify: t.notify, dailySummaryAt: t.dailySummaryAt, quietHours: t.quietHours,
323
323
  audit: { days: chain.length, broken: chain.filter((c) => !c.check.ok).map((c) => c.day) },
324
+ // task-1645: the real-mode trade-fire channels live in the same file (dexterm autotrade signal channels).
325
+ signals: (() => { const sc = parseSignalsConfig(cfg.signals); return { channels: enabledChannels(sc.config), telegramCopy: sc.config.telegram.enabled, ttlSec: sc.config.ttlSec, problems: sc.problems }; })(),
324
326
  };
325
327
  if (argv.includes('--json')) {
326
328
  io.stdout.write(`${JSON.stringify(view, null, 2)}\n`);
@@ -332,6 +334,7 @@ export async function telegramCommand(argv, io, deps = {}) {
332
334
  io.stdout.write(` approvals ${t.approvals} · expiry ${t.approvalExpirySec} s · re-quote tolerance ${t.repriceTolPct}%\n`);
333
335
  io.stdout.write(` notify candidates ${t.notify.candidates} · entries ${t.notify.entries ? 'on' : 'off'} · closes ${t.notify.closes ? 'on' : 'off'} · halts ${t.notify.halts ? 'on' : 'off'} · spend ${t.notify.spend ? 'on' : 'off'} · daily ${t.notify.daily ? `at ${t.dailySummaryAt ?? 'off'}` : 'off'}${t.quietHours ? ` · quiet ${t.quietHours}` : ''}\n`);
334
336
  io.stdout.write(` audit ${chain.length} day file${chain.length === 1 ? '' : 's'}${view.audit.broken.length ? ` · BROKEN: ${view.audit.broken.join(', ')}` : ''}\n`);
337
+ io.stdout.write(` trade-fire signals (real mode) · channels ${view.signals.channels.filter((c) => c !== 'telegram').join(', ') || 'none'} · Telegram copy ${view.signals.telegramCopy ? 'on' : 'off'} · ttl ${view.signals.ttlSec} s · set with dexterm autotrade signal channels${view.signals.problems.length ? ` · OFF: ${view.signals.problems.join('; ')}` : ''}\n`);
335
338
  return 0;
336
339
  }
337
340
  case 'approvals': {
@@ -1,3 +1,3 @@
1
1
  // The one place the app's version lives (task-1397). --version, the help banner and `dexterm update` read it;
2
2
  // cli.test.ts keeps it equal to packages/cli/package.json (release.yml refuses a tag that differs).
3
- export const VERSION = '0.1.7';
3
+ export const VERSION = '0.2.1';