dexterm-cli 0.1.7 → 0.2.0

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 (119) hide show
  1. package/dist/packages/agents/src/spectator.js +3 -1
  2. package/dist/packages/agents/src/tiers.js +9 -0
  3. package/dist/packages/agents/src/tools.js +10 -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 +2 -1
  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 +3 -0
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +19 -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/runner.js +62 -11
  30. package/dist/packages/autotrade/src/service-guard.js +125 -0
  31. package/dist/packages/autotrade/src/service-install.js +213 -0
  32. package/dist/packages/autotrade/src/service-recover.js +232 -0
  33. package/dist/packages/autotrade/src/service.js +289 -0
  34. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  35. package/dist/packages/autotrade/src/signal.js +196 -0
  36. package/dist/packages/autotrade/src/strategy.js +3 -1
  37. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  38. package/dist/packages/cli/src/analyst/backend.js +3 -2
  39. package/dist/packages/cli/src/analyst/codex.js +3 -2
  40. package/dist/packages/cli/src/app-help-gen.js +14 -2
  41. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  42. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  43. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  44. package/dist/packages/cli/src/autotrade-cmd/fleet.js +8 -4
  45. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  46. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  47. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  48. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  49. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  50. package/dist/packages/cli/src/autotrade-cmd/run.js +257 -13
  51. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  52. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  53. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  54. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  55. package/dist/packages/cli/src/pnl-card.js +4 -0
  56. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  57. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  58. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  59. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  60. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  61. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  62. package/dist/packages/cli/src/version.js +1 -1
  63. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  64. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  65. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  66. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  67. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  68. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  69. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  70. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  71. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  72. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  73. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  74. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  75. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
  76. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  77. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  78. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  79. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  80. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  81. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  83. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
  84. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  85. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  86. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
  87. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  88. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  89. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  90. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  91. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  92. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  93. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  94. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  95. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  96. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  97. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  98. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  99. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  100. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  101. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  102. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  105. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  107. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  109. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  110. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  111. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  112. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  113. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  114. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  115. package/dist/packages/mcp/src/index.js +1 -1
  116. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  117. package/dist/packages/mcp/src/server.js +21 -10
  118. package/dist/packages/tui/src/keymap.js +7 -0
  119. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ ---
2
+ id: docs-real-mode-the-signal
3
+ title: Real mode (LIVE TRADE FIRE signals) › The signal
4
+ kind: docs
5
+ page: real-mode
6
+ page_title: Real mode (LIVE TRADE FIRE signals)
7
+ heading: The signal
8
+ anchor: the-signal
9
+ url: https://dexterm.xyz/docs/real-mode#the-signal
10
+ tags:
11
+ - docs
12
+ - real-mode
13
+ - signal
14
+ - real
15
+ - mode
16
+ - live
17
+ - trade
18
+ - fire
19
+ - signals
20
+ confidence: high
21
+ owner: maintainers
22
+ origin: docs-generated
23
+ reviewed_by: generated@2026-10-16
24
+ generated_from: dexterm/site content/docs (task-1420)
25
+ sources:
26
+ - site/content/docs/real-mode.md
27
+ date: 2026-10-16
28
+ ---
29
+
30
+ One line of JSON, the same bytes on every channel:
31
+
32
+ ```json
33
+ {"schema":"dexterm-trade-fire/1","type":"trade","id":"9f2c…(32 hex)","nonce":42,"issuedAt":1792000000000,"expiresAt":1792000060000,"ttlSec":60,"runId":"…","accountId":"main","strategy":{"id":"my-strategy","version":"1.0.1","hash":"…"},"paper":false,"side":"buy","mint":"…","symbol":"PEPE","sizeSol":0.05,"sizeUsd":10,"pctOfBook":2,"sellFraction":null,"price":{"usd":0.0012,"mcap":1200000,"liqUsd":48000,"solUsd":200},"exitReason":null,"slippageBps":300,"maxImpactPct":3,"positionId":"…"}
34
+ ```
35
+
36
+ - `type`: `trade`, `halted` (the run ended or stopped: `outstanding` lists the nonces it had sent that had not expired, `open` the coins dexterm still counts as held for the account) or `test`.
37
+ - `side` `buy` or `sell`. A buy states `sizeSol` (what to spend) and `pctOfBook`. A sell states `sellFraction`, the share of the position to sell (apply it to what you really hold), and `exitReason` (the exit rule that fired: `hard-stop`, `trail`, `tp-1`, `agent-exit`, `flatten` and so on); its `sizeSol` is dexterm's modelled proceeds and can be 0 for a coin that rugged.
38
+ - `nonce` only ever grows, across runs and restarts. `id` is the idempotency key (a hash of the account, the run and the nonce). `paper` is always `false`.
39
+ - **Firewall**: the only token-made text in a signal is the symbol, reduced to `A-Z a-z 0-9 . _ $ -` (16 characters). No name, no description, no link, no agent text, no path, no key.
40
+
41
+ **What a well-behaved executor does with each signal:**
42
+
43
+ 1. Check the HMAC (when you set a secret): `x-dexterm-signature: t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 of `<t>.<body>` with your secret. Refuse a timestamp more than 5 minutes off.
44
+ 2. Ignore it after `expiresAt`. A late signal is never acted on.
45
+ 3. Act once per `nonce` (delivery is at least once: a repeat is normal) and ignore a nonce lower than one already acted on.
46
+ 4. Act only on the `accountId` it was set up for.
47
+
48
+ `SignalReceiver` in `@dexterm/autotrade` does these four checks, if your executor is written in Node.
@@ -0,0 +1,35 @@
1
+ ---
2
+ id: docs-real-mode-what-it-is
3
+ title: Real mode (LIVE TRADE FIRE signals) › What it is
4
+ kind: docs
5
+ page: real-mode
6
+ page_title: Real mode (LIVE TRADE FIRE signals)
7
+ heading: What it is
8
+ anchor: what-it-is
9
+ url: https://dexterm.xyz/docs/real-mode#what-it-is
10
+ tags:
11
+ - docs
12
+ - real-mode
13
+ - real
14
+ - mode
15
+ - live
16
+ - trade
17
+ - fire
18
+ - signals
19
+ confidence: high
20
+ owner: maintainers
21
+ origin: docs-generated
22
+ reviewed_by: generated@2026-10-16
23
+ generated_from: dexterm/site content/docs (task-1420)
24
+ sources:
25
+ - site/content/docs/real-mode.md
26
+ date: 2026-10-16
27
+ ---
28
+
29
+ > **0.2.0, experimental.** Real mode can lead to real trades in a wallet you fund. dexterm never holds a key, never signs and never sends a transaction: your executor does, by its own rules. Not financial advice.
30
+
31
+ Every strategy runs on **paper** by default. In **real** mode the same strategy runs the same way: the same entry rules, the same clamp on every size, the same exits, the same monitor and the same run file as paper. The difference: every buy and sell it fills on its own model of the market also goes out, first, as a **LIVE TRADE FIRE** signal (`dexterm-trade-fire/1`) to the executor you chose.
32
+
33
+ - **Your executor** is a Telegram trading bot you trust, or any script of yours. It holds its own wallet and decides whether to act on a signal.
34
+ - **dexterm** holds no key, signs nothing and builds no transaction. It sends the signal, keeps it in an outbox until it is delivered, and records what your executor says it did.
35
+ - **A bot cannot read another bot's messages**, so the signal also goes over a channel your executor can read: a local webhook, a command, a stream file or MCP. The Telegram message to your own chat is the copy for you.
@@ -0,0 +1,34 @@
1
+ ---
2
+ id: docs-real-mode-what-it-will-not-do
3
+ title: Real mode (LIVE TRADE FIRE signals) › What it will not do
4
+ kind: docs
5
+ page: real-mode
6
+ page_title: Real mode (LIVE TRADE FIRE signals)
7
+ heading: What it will not do
8
+ anchor: what-it-will-not-do
9
+ url: https://dexterm.xyz/docs/real-mode#what-it-will-not-do
10
+ tags:
11
+ - docs
12
+ - real-mode
13
+ - will
14
+ - real
15
+ - mode
16
+ - live
17
+ - trade
18
+ - fire
19
+ - signals
20
+ confidence: high
21
+ owner: maintainers
22
+ origin: docs-generated
23
+ reviewed_by: generated@2026-10-16
24
+ generated_from: dexterm/site content/docs (task-1420)
25
+ sources:
26
+ - site/content/docs/real-mode.md
27
+ date: 2026-10-16
28
+ ---
29
+
30
+ - Hold, import or ask for a key or a seed phrase, sign, or send a transaction.
31
+ - Trade on a replay, or let a chat tap authorise a real trade.
32
+ - Promise a fill. Your executor decides, at its own price, with its own fees.
33
+ - Run more than one real strategy per machine at a time, run one inside `run --all`, or run one under the always-on service (in 0.2.0 real runs in the foreground only).
34
+ - Send any signal, sells included, while no real run is running, or close a position by itself.
@@ -0,0 +1,34 @@
1
+ ---
2
+ id: docs-real-mode-where-you-see-it
3
+ title: Real mode (LIVE TRADE FIRE signals) › Where you see it
4
+ kind: docs
5
+ page: real-mode
6
+ page_title: Real mode (LIVE TRADE FIRE signals)
7
+ heading: Where you see it
8
+ anchor: where-you-see-it
9
+ url: https://dexterm.xyz/docs/real-mode#where-you-see-it
10
+ tags:
11
+ - docs
12
+ - real-mode
13
+ - where
14
+ - real
15
+ - mode
16
+ - live
17
+ - trade
18
+ - fire
19
+ - signals
20
+ confidence: high
21
+ owner: maintainers
22
+ origin: docs-generated
23
+ reviewed_by: generated@2026-10-16
24
+ generated_from: dexterm/site content/docs (task-1420)
25
+ sources:
26
+ - site/content/docs/real-mode.md
27
+ date: 2026-10-16
28
+ ---
29
+
30
+ - **The run**: every fill and closed trade row of a real run is tagged `real-signal` and names its nonce; the start line says REAL; receipts become rows; the end has a `real_halted` row.
31
+ - **The `Q` screen's runner view**: the mode, the account, the kill switch, the outbox depth, each channel's health, the unconfirmed positions, the limits in force (with a BUYS PAUSED line when a limit or a silent executor stops new buys) and your executor's health.
32
+ - **Telegram**: every message of a real run carries the LIVE badge. A closed trade is a text line (its numbers are dexterm's model, not your executor's fills), never a PnL card.
33
+ - **The trade ledger**: `dexterm ledger` lists a real run's trades with the mode `real` (`--mode real`), never `paper`. Their numbers are dexterm's model of each trade it signalled; your executor's receipts are the real fills, so there is no PnL card for them.
34
+ - **Shared views**: the spectator stream and saved files keep signals and receipts out.
@@ -0,0 +1,27 @@
1
+ ---
2
+ id: docs-real-mode
3
+ title: Real mode (LIVE TRADE FIRE signals)
4
+ kind: docs
5
+ page: real-mode
6
+ page_title: Real mode (LIVE TRADE FIRE signals)
7
+ url: https://dexterm.xyz/docs/real-mode
8
+ tags:
9
+ - docs
10
+ - real-mode
11
+ - real
12
+ - mode
13
+ - live
14
+ - trade
15
+ - fire
16
+ - signals
17
+ confidence: high
18
+ owner: maintainers
19
+ origin: docs-generated
20
+ reviewed_by: generated@2026-10-16
21
+ generated_from: dexterm/site content/docs (task-1420)
22
+ sources:
23
+ - site/content/docs/real-mode.md
24
+ date: 2026-10-16
25
+ ---
26
+
27
+ Run a strategy in real mode and every buy and sell it decides goes to your own executor (a Telegram trading bot of your choice, or any script) as a signed, numbered LIVE TRADE FIRE signal. dexterm still holds no key and signs nothing. Experimental; not financial advice.
@@ -30,4 +30,4 @@ It does three things, each one a tier you grant when you pair:
30
30
  - **Control** (https://dexterm.xyz/docs/telegram#control-the-commands): `/status`, `/positions`, `/pause`, `/resume`, `/kill`, `/flatten`, `/strategies`, `/use` and `/approvals on`. Chat can always make things **safer** and never less safe.
31
31
  - **Approve** (https://dexterm.xyz/docs/telegram#approve-one-tap-entries): every new entry waits for your tap, with [Approve] [Half size] [Skip] buttons. Only a button approves; a typed message never does.
32
32
 
33
- It runs on **paper**: an approved entry fills on the paper book, with no key, wallet or signer anywhere. This is also dexterm's part in live trading (https://dexterm.xyz/docs/live-trading): dexterm holds no key, and live trades happen in a separate Telegram trading bot of your choice, which holds its own wallet. dexterm sends the alerts and run control to your chat; in this version it does not place orders in that trading bot for you. Experimental, not financial advice.
33
+ It runs on **paper**: an approved entry fills on the paper book, with no key, wallet or signer anywhere. This is also dexterm's part in live trading (https://dexterm.xyz/docs/live-trading): dexterm holds no key, and live trades happen in a separate Telegram trading bot of your choice, which holds its own wallet. dexterm sends the alerts and run control to your chat; in this version it does not place orders in that trading bot for you. In real mode (https://dexterm.xyz/docs/real-mode) (coming in 0.2.0) your chat also gets a LIVE TRADE FIRE copy of each signal, and the channels your executor reads are set up in the same file as this bot (`dexterm autotrade signal channels`; `dexterm telegram status` shows them). Experimental, not financial advice.
@@ -7,7 +7,7 @@ tags:
7
7
  - paper
8
8
  - backtest
9
9
  - proposal
10
- date: 2026-10-21
10
+ date: 2026-10-24
11
11
  confidence: high
12
12
  owner: maintainers
13
13
  origin: primary-doc
@@ -33,6 +33,8 @@ sources:
33
33
  - "stricter coins" → add a screen clause (`top10 <= 25`) or tighten `/entry/safety/*`.
34
34
  - "stop after a bad day" → lower `/risk/dailyLossHaltPct` or `/risk/drawdown/haltAtPct`.
35
35
 
36
- **Then measure.** `strategy_backtest {proposalId}` runs the current version and the proposal on the same recorded tape (days <= 14, read-only, rate-limited). Quote the trade count and the data limits: small n means little, fills are modelled, and early-entry rugs are often unsellable. A backtest is never a prediction and never financial advice.
36
+ **Then measure.** `strategy_backtest {proposalId}` runs the current version and the proposal on the same recorded tape (days <= 14, rate-limited; a proposal's backtest saves nothing). `strategy_backtest {id}` also runs the strategy on each half of the held-out split and records it as the strategy's backtest evidence. Quote the trade count and the data limits: small n means little, fills are modelled, and early-entry rugs are often unsellable. A backtest is never a prediction and never financial advice.
37
37
 
38
38
  **Status.** `autotrade_status` shows the paper runner (heartbeat, halt, candidates, sessions and spend, positions) and the proposals waiting for the user.
39
+
40
+ **The journey (one rung at a time).** draft → backtested (`strategy_backtest {id}`: held-out split recorded) → forward-tested (`strategy_forward_start {id, hours|trades}`: paper, live data, its own bankroll; read `strategy_forward_report` when it ends; an early stop never counts) → always-on → real (`strategy_promote_propose {id, to}`). `strategy_journey {id}` shows the rungs; evidence binds to the exact rules. Never skip a rung, apply a promotion, set or suggest caps, pick a bankroll or arm anything: say "I proposed it; you apply it". Claim gains only when `heldOut.claim` / `comparison.claim` is true (>= 20 trades in both halves, net above 0); otherwise "too few trades to claim anything".
@@ -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";