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
@@ -12,7 +12,8 @@
12
12
  // never sleep (the source paces itself) and replies land when they land.
13
13
  //
14
14
  // Every step lands in the hash-chained run ledger (ledger.ts) with the exact inputs (the tick, the message text, the
15
- // raw reply, the clamp). Paper only: a strategy whose mode is not "paper" is refused before the first tick. The
15
+ // raw reply, the clamp). Paper, or real (task-1645: the paper engine plus a trade-fire signal per fill, through the
16
+ // real-mode hook only); any other mode is refused before the first tick. The
16
17
  // sessions' day caps and allowance guard live in SessionGate (sessions.ts); with none given, the strategy's own
17
18
  // limits apply; the gate counts real usage per wall-clock day, so a fast --replay over many tape days never resets
18
19
  // the $/day cap. The host part at the end of this file (lock, rendezvous, heartbeat) is what the CLI uses.
@@ -33,14 +34,24 @@ export function simExecutor(x) {
33
34
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
34
35
  const REVIEW = ['time_review', 'time_tighten'];
35
36
  const INFO_ONLY = ['mechanical_exit', 'stuck'];
37
+ /** Session ends that fail the same way every time: no new session for the rest of the run. */
38
+ const GUARD_REASONS = new Set(['account-mismatch', 'not-tool-less', 'account-unavailable']);
36
39
  async function* fromArray(ticks) { for (const k of ticks)
37
40
  yield k; }
38
41
  export async function runAutotrade(o) {
39
42
  const s = o.strategy;
40
43
  // task-1571: autotrade runs on paper only. dexterm holds no key and places no live order; live trading happens in the
41
44
  // Telegram trading bot of the user's choice.
42
- if (s.mode !== 'paper')
43
- throw new Error(`strategy ${s.id} is mode "${s.mode}": autotrade runs paper only (dexterm holds no key and places no live order)`);
45
+ // task-1645: mode "real" = the same paper engine, plus a LIVE TRADE FIRE signal per fill for the user's own executor;
46
+ // it needs the real-mode hook (armed on the user's terminal, checked by the host) and never runs without it.
47
+ if (s.mode === 'real' && !o.real)
48
+ throw new Error(`strategy ${s.id} is mode "real": it runs only through the real-mode host (dexterm autotrade run ${s.id} --real, once armed on your own terminal)`);
49
+ if (s.mode !== 'paper' && s.mode !== 'real')
50
+ throw new Error(`strategy ${s.id} is mode "${s.mode}": autotrade runs paper only (dexterm holds no key and places no live order); mode real runs only through the real-mode host`);
51
+ if (o.real && s.mode !== 'real')
52
+ throw new Error(`strategy ${s.id} is mode "${s.mode}": the real-mode hook is for a strategy in mode "real" only`);
53
+ if (o.real && (!o.executor || o.executor !== o.real))
54
+ throw new Error('real mode: the executor must be the real-mode signal executor itself');
44
55
  if (!o.tape && !o.source)
45
56
  throw new Error('runAutotrade needs a tape or a live source');
46
57
  const ledger = o.ledger ?? new Ledger();
@@ -86,31 +97,131 @@ export async function runAutotrade(o) {
86
97
  const pumps = new Set();
87
98
  /** Proposals waiting for an answer, by mint (task-1527). */
88
99
  const approvalsOpen = new Map();
89
- ledger.add(t0, 'run_start', { strategy: s.id, version: s.version, strategyHash: s.hash, mode: s.mode, ...(s.highRisk ? { highRisk: true } : {}), tape: o.tape?.source ?? o.source.name, ticks: tapeTicks?.length ?? null, decide: decideAuto ? 'auto' : s.entry.decide, ...(o.approvals ? { approvals: o.approvals.required(s) ? 'required' : 'when-asked' } : {}), pacing: pacing.mode, executor: exec.name, caps: capsView(gate), ...(o.account ? { account: o.account.id } : {}) });
100
+ ledger.add(t0, 'run_start', { strategy: s.id, version: s.version, strategyHash: s.hash, mode: s.mode, ...(o.real ? { tag: 'real-signal', accountId: o.real.accountId } : {}), ...(s.highRisk ? { highRisk: true } : {}), tape: o.tape?.source ?? o.source.name, ticks: tapeTicks?.length ?? null, decide: decideAuto ? 'auto' : s.entry.decide, ...(o.approvals ? { approvals: o.approvals.required(s) ? 'required' : 'when-asked' } : {}), pacing: pacing.mode, executor: exec.name, caps: capsView(gate), ...(o.account ? { account: o.account.id } : {}) });
101
+ // task-1645 r2: a real run takes over the real positions an earlier run of this strategy left open (held.json),
102
+ // so their exits keep firing. A profile the strategy no longer has falls back to its default profile.
103
+ for (const p of o.real?.inherited() ?? []) {
104
+ const from = p.profileId;
105
+ if (!s.exits.profiles[p.profileId])
106
+ p.profileId = s.exits.default;
107
+ if (!book.adopt(p))
108
+ continue;
109
+ ledger.add(t0, 'position_adopted', { tag: 'real-signal', positionId: p.id, mint: p.mint, symbol: p.symbol, ...(from !== p.profileId ? { profileFrom: from } : {}), position: { ...p, fills: p.fills } });
110
+ say(`CARRIED ${p.symbol} (${p.mint.slice(0, 4)}…): real position from an earlier run, ${p.profileId}, cost $${(p.costUsd - p.realizedUsd).toFixed(2)} still open; its exits keep running`);
111
+ }
112
+ // 0.2.0 card B: positions a crashed run left open. A profile the (re-read) strategy no longer has falls back to
113
+ // its default profile, so every adopted position has exits.
114
+ for (const p of o.adopt ?? []) {
115
+ const from = p.profileId;
116
+ if (!s.exits.profiles[p.profileId])
117
+ p.profileId = s.exits.default;
118
+ if (!book.adopt(p))
119
+ continue;
120
+ ledger.add(t0, 'position_adopted', { positionId: p.id, mint: p.mint, symbol: p.symbol, ...(from !== p.profileId ? { profileFrom: from } : {}), position: { ...p, fills: p.fills } });
121
+ say(`RESUMED ${p.symbol} (${p.mint.slice(0, 4)}…): ${p.profileId}, cost $${(p.costUsd - p.realizedUsd).toFixed(2)} still open; its exits keep running`);
122
+ }
90
123
  const count = (rule) => { refusals[rule] = (refusals[rule] ?? 0) + 1; };
91
124
  const refuse = (rule, data) => { count(rule); ledger.add(now, 'refused', { rule, ...data }); };
92
125
  // A vet that throws or hangs is a check that could not run: the strategy's `unknown` decides (reject by default).
93
- const vetCoin = async (k) => {
126
+ const vetCoin = async (k, ctx) => {
94
127
  let r;
95
128
  try {
96
- r = await o.vet(k);
129
+ r = await o.vet(k, ctx);
97
130
  }
98
131
  catch (e) {
99
132
  r = { ok: false, rule: 'vet-error', detail: e.message.slice(0, 160) };
100
133
  }
101
134
  if (r.ok) {
102
- vetted.add(k.mint);
135
+ if (!r.recheck)
136
+ vetted.add(k.mint);
103
137
  if (r.note)
104
- ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200) });
138
+ ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200), ...(r.sizeCapPct !== undefined ? { sizeCapPct: r.sizeCapPct } : {}) });
105
139
  }
106
140
  return r;
107
141
  };
142
+ const vetCtx = (k, sizeUsd, h) => ({ history: h, sizeUsd, exitFloorUsd: x.minExitLiquidityUsd, maxTop10Pct: s.entry.safety.maxTop10Pct, maxDevPct: s.entry.safety.maxDevPct, unknownHolders: s.entry.safety.unknownHolders });
143
+ const drainGuard = s.entry.safety.rugproof?.drainGuard ?? null;
144
+ const stuckSince = new Map();
145
+ /** The liquidity read each position was bought at (the drain guard's base). */
146
+ const entryLiq = new Map();
147
+ // Paper only: a real-signal bag is still in the user's wallet, so it keeps its exit signals and carries to the next
148
+ // real run (validateStrategy also refuses the option outside mode paper).
149
+ const writeOffMin = !o.real && s.mode === 'paper' ? x.stuckWriteOffMin ?? null : null;
150
+ /** Positions the pool probe saw drained (no feed-priced retry while it still says so). */
151
+ const drained = new Set();
152
+ /**
153
+ * task-1748, every step, for every open position:
154
+ * - the drain guard: an exit verdict from the pool poller (SOL vault / Jupiter, <= 10 s apart), or this coin's own
155
+ * feed liquidity down more than drainGuard.liqDropPct from the entry read, sells the whole position at once. The
156
+ * sale is priced on the liquidity the guard saw (0 when the route is gone), not on a lagging feed row: an
157
+ * emptied pool leaves nothing to sell into, the bag goes stuck and the write-off below closes it.
158
+ * - execution.stuckWriteOffMin (paper): a bag stuck that long is closed at its realizable value (sells already
159
+ * made kept, the rest 0), with a written_off event and a trade_closed line, so crash recovery never adopts it.
160
+ */
161
+ const sweepOpen = async (tick) => {
162
+ for (const p of [...book.open.values()]) {
163
+ if (p.status !== 'open')
164
+ continue;
165
+ const k = latest.get(p.mint) ?? p.lastTick;
166
+ if (drainGuard && !p.stuck) {
167
+ const v = o.drain?.verdict(p.id) ?? null;
168
+ const base = entryLiq.has(p.id) ? entryLiq.get(p.id) ?? null : (p.liqWindow[0]?.liq || null);
169
+ const own = tick && tick.mint === p.mint && base && tick.liq !== null && (1 - tick.liq / base) * 100 > drainGuard.liqDropPct
170
+ ? { reason: 'drain-liquidity', detail: `liquidity $${Math.round(base)} -> $${Math.round(tick.liq)} since entry (-${((1 - tick.liq / base) * 100).toFixed(1)}% > ${drainGuard.liqDropPct}%)`, liqUsd: tick.liq } : null;
171
+ const hit = v ?? own;
172
+ if (hit) {
173
+ ledger.add(now, 'drain_guard', { positionId: p.id, mint: p.mint, reason: hit.reason, detail: hit.detail, liqUsd: hit.liqUsd, source: v ? 'pool-probe' : 'feed' });
174
+ say(`DRAIN GUARD ${p.symbol}: ${hit.detail}`);
175
+ await sellPosition(p, { ...k, t: now, liq: hit.liqUsd ?? 0 }, p.tokens, hit.reason, hit.detail);
176
+ // The probe saw the pool emptied or the route gone: the lagging feed must not "sell" the bag at its old
177
+ // liquidity. While the probe still says so, the monitor's stuck retry is off for this position.
178
+ if (v && p.status === 'open' && p.stuck)
179
+ drained.add(p.id);
180
+ }
181
+ }
182
+ const since = stuckSince.get(p.id);
183
+ if (writeOffMin !== null && p.status === 'open' && p.stuck && since !== undefined && now - since >= writeOffMin * 60e3) {
184
+ const ct = book.close(p, now, 'written-off', true);
185
+ o.drain?.unwatch(p.id);
186
+ stuckSince.delete(p.id);
187
+ ledger.add(now, 'written_off', { positionId: p.id, mint: p.mint, stuckMin: Math.round((now - since) / 6e3) / 10, realizedUsd: ct.proceedsUsd, costUsd: ct.costUsd, why: `stuck ${writeOffMin} min under the exit floor: closed at realizable value (the rest counts 0)` });
188
+ ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, writtenOff: true });
189
+ say(`WRITTEN OFF ${p.symbol}: stuck ${writeOffMin} min, ${ct.pnlPct}% realized`);
190
+ }
191
+ if (p.status === 'closed') {
192
+ const c = byPosition.get(p.id);
193
+ if (c) {
194
+ if (c.inflight)
195
+ await c.inflight;
196
+ await closeCandidate(c, 'position-closed');
197
+ }
198
+ }
199
+ }
200
+ };
108
201
  const liveCandidateSessions = () => [...cands.values()].filter((q) => q.session && !q.positionId).length;
109
202
  const everyLive = () => [...new Set([...cands.values(), ...byPosition.values()])];
110
203
  const newCand = (k, matched, session, kind, positionId) => ({
111
204
  mint: k.mint, symbol: k.symbol, since: now, lastMatch: now, matched, session, sessionKind: kind, lastSent: view(k), lastSentT: now, seq: 0,
112
205
  inflight: null, pendingText: [], notes: [], closing: false, positionId, awaitingReview: null, oneShot: kind === 'position', costSeen: 0, turns: 0, lastDecision: null,
113
206
  });
207
+ /**
208
+ * Why the session's process ended on its own (null while alive). A session that proved the account or its tool list
209
+ * wrong (or had no account) blocks new sessions for the run; the guard is logged once, by the session that set it.
210
+ * Checked on every path that learns of a death: a null reply AND a close (a candidate's TTL can close a session that
211
+ * already died, before its null reply is read; task-1712).
212
+ */
213
+ let guardLogged = false;
214
+ const sessionDied = (c) => {
215
+ const died = c.session?.endReason?.() ?? null;
216
+ if (c.session && died && GUARD_REASONS.has(died)) {
217
+ if (!guardLogged && (gate.blocked === null || gate.blockedBy === c.session.id)) {
218
+ guardLogged = true;
219
+ ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
220
+ }
221
+ gate.block(died, c.session.id);
222
+ }
223
+ return died;
224
+ };
114
225
  const closeCandidate = async (c, reason) => {
115
226
  if (c.closing)
116
227
  return;
@@ -132,8 +243,11 @@ export async function runAutotrade(o) {
132
243
  c.costSeen = st.costUsd;
133
244
  }
134
245
  sessionCost += st.costUsd;
135
- ledger.add(now, 'session_end', { sessionId: c.session.id, sessionKind: c.sessionKind, mint: c.mint, reason, turns: st.turns, costUsd: st.costUsd });
136
- say(`session ${c.session.id} ended (${reason}) · ${st.turns} turns · $${st.costUsd.toFixed(4)}`);
246
+ // A session that had already failed its proof ended for that reason, whatever closed its candidate.
247
+ const died = sessionDied(c);
248
+ const why = died && GUARD_REASONS.has(died) ? died : reason;
249
+ ledger.add(now, 'session_end', { sessionId: c.session.id, sessionKind: c.sessionKind, mint: c.mint, reason: why, turns: st.turns, costUsd: st.costUsd });
250
+ say(`session ${c.session.id} ended (${why}) · ${st.turns} turns · $${st.costUsd.toFixed(4)}`);
137
251
  }
138
252
  };
139
253
  const sellPosition = async (p, k, tokens, reason, detail) => {
@@ -143,21 +257,37 @@ export async function runAutotrade(o) {
143
257
  if ('refused' in f) {
144
258
  if (!p.stuck) {
145
259
  p.stuck = true;
260
+ stuckSince.set(p.id, now);
146
261
  ledger.add(now, 'stuck', { positionId: p.id, mint: p.mint, why: f.refused, reason });
147
262
  say(`STUCK ${p.symbol}: cannot sell (${f.refused})`);
148
263
  }
149
264
  return;
150
265
  }
151
266
  p.stuck = false;
152
- ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), detail });
267
+ stuckSince.delete(p.id);
268
+ ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), detail, ...sigTag(f) });
153
269
  say(`SELL ${p.symbol} ${reason}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)} (impact ${f.impactPct}%) — ${detail}`);
154
270
  const ct = book.sell(p, f);
271
+ if (ct)
272
+ o.drain?.unwatch(p.id);
155
273
  if (ct) {
156
274
  // task-1530: the price extremes the monitor saw while held (the trade ledger's MFE / MAE)
157
- ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null });
275
+ ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, ...(o.real ? { tag: 'real-signal' } : {}) });
158
276
  say(`CLOSED ${ct.symbol}: ${ct.pnlUsd >= 0 ? '+' : ''}$${ct.pnlUsd.toFixed(2)} (${ct.pnlPct}%) by ${ct.exitReason}`);
159
277
  }
160
278
  };
279
+ /** task-1748: a young or partly verified pool takes at most the vet's sizeCapPct of the strategy's pick. */
280
+ const capYoungPool = (r, c) => {
281
+ if (!r.ok || c.sizeCapPct === undefined)
282
+ return r;
283
+ const capUsd = Math.round(r.usd * c.sizeCapPct) / 100;
284
+ if (capUsd >= r.usd)
285
+ return r;
286
+ if (capUsd < s.sizing.minUsd)
287
+ return { ok: false, refused: 'below-min-after-rugproof-young-pool', clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
288
+ const f = r.usd ? capUsd / r.usd : 1;
289
+ return { ...r, usd: capUsd, firstTrancheUsd: Math.round(r.firstTrancheUsd * f * 100) / 100, clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
290
+ };
161
291
  /** Apply a validated decision at the current virtual time. */
162
292
  const apply = async (c, d) => {
163
293
  const k = latest.get(c.mint);
@@ -182,7 +312,7 @@ export async function runAutotrade(o) {
182
312
  return;
183
313
  }
184
314
  const req = { profile: d.profile, sizePct: d.sizePct, sizeUsd: d.sizeUsd, confidence: d.confidence };
185
- const r = clampEntry(book, k, req, killed);
315
+ const r = capYoungPool(clampEntry(book, k, req, killed), c);
186
316
  ledger.add(now, 'clamp', { mint: c.mint, sessionId: c.session?.id ?? null, request: req, result: r.ok ? { ok: true, usd: r.usd, firstTrancheUsd: r.firstTrancheUsd, profile: r.profile.id, basis: r.basis, clamps: r.clamps } : r });
187
317
  for (const cl of r.clamps)
188
318
  clampCount[cl.rule] = (clampCount[cl.rule] ?? 0) + 1;
@@ -325,14 +455,9 @@ export async function runAutotrade(o) {
325
455
  c.costSeen = Math.max(c.costSeen, r.costUsd);
326
456
  }
327
457
  if (!r || !r.decision.ok) {
328
- const died = c.session.endReason?.() ?? null;
458
+ const died = sessionDied(c);
329
459
  if (!r)
330
460
  ledger.add(now, 'session_timeout', { sessionId: c.session.id, mint: c.mint, ended: died });
331
- if (died === 'account-mismatch' || died === 'not-tool-less' || died === 'account-unavailable') {
332
- if (!gate.blocked)
333
- ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
334
- gate.block(died);
335
- }
336
461
  if (review)
337
462
  await noAnswer(review, !r ? (died ?? 'timeout') : 'invalid-decision');
338
463
  if (!r) {
@@ -436,16 +561,18 @@ export async function runAutotrade(o) {
436
561
  f.reason = 'ladder-add';
437
562
  const bookUsd = round4(book.bookEquity()); // task-1530: the book before the fill (the trade ledger's % of book)
438
563
  book.addTranche(p, f);
439
- ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), bookUsd });
564
+ ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), bookUsd, ...sigTag(f) });
440
565
  say(`ADD ${k.symbol}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}`);
441
566
  continue;
442
567
  }
443
568
  const prof = s.exits.profiles[q.profile];
444
569
  const bookUsd = round4(book.bookEquity()); // task-1530: the book before the fill (the trade ledger's % of book)
445
570
  const p = book.openPosition(k, f, prof, q.sessionId, q.trigger, q.ladder);
446
- ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, profile: prof.id, ...fillMcaps(fillView(f), k), bookUsd, ...(q.ctx?.proposalId ? { proposalId: q.ctx.proposalId } : {}) });
571
+ ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, profile: prof.id, ...fillMcaps(fillView(f), k), bookUsd, ...(q.ctx?.proposalId ? { proposalId: q.ctx.proposalId } : {}), ...sigTag(f) });
447
572
  say(`BUY ${k.symbol} [${prof.id}]: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}${f.partial ? ' (PARTIAL: impact cap)' : ''} · liq $${Math.round(k.liq ?? 0)}`);
448
573
  const c = cands.get(q.mint);
574
+ entryLiq.set(p.id, k.liq ?? null);
575
+ o.drain?.watch({ positionId: p.id, mint: p.mint, entryLiqUsd: k.liq ?? null, poolAddress: c?.pool?.address ?? null, quoteVault: c?.pool?.quoteVault ?? null, tokens: () => p.tokens });
449
576
  if (c) {
450
577
  c.positionId = p.id;
451
578
  byPosition.set(p.id, c);
@@ -525,18 +652,25 @@ export async function runAutotrade(o) {
525
652
  sessions: live.map((c) => ({ id: c.session.id, kind: c.sessionKind, mint: c.mint, symbol: c.symbol, turns: c.turns, costUsd: round4(c.session.costUsd()), budgetUsd: gate.caps.budgetUsdPerSession, lastDecision: c.lastDecision })),
526
653
  positions: [...book.open.values()].map((p) => {
527
654
  const pnl = liquidationMark(p) + p.realizedUsd - p.costUsd;
528
- return { mint: p.mint, symbol: p.symbol, profile: p.profileId, costUsd: round2(p.costUsd), pnlUsd: round2(pnl), pnlPct: p.costUsd ? Math.round((pnl / p.costUsd) * 1000) / 10 : 0, next: p.stuck ? 'stuck: retried every tick' : null, ...(p.costRecovered ? { freeBag: true } : {}) };
655
+ return { mint: p.mint, symbol: p.symbol, profile: p.profileId, costUsd: round2(p.costUsd), pnlUsd: round2(pnl), pnlPct: p.costUsd ? Math.round((pnl / p.costUsd) * 1000) / 10 : 0, next: p.stuck ? 'stuck: retried every tick' : null, ...(p.costRecovered ? { freeBag: true } : {}), ...(o.real ? { confirm: o.real.confirmOf(p.mint) ?? 'unconfirmed' } : {}) };
529
656
  }),
530
657
  ledgerTail: ledger.events.slice(-12).map((e) => ({ seq: e.seq, t: e.t, kind: e.kind, text: tailText(e) })),
531
658
  ledgerHead: ledger.head(),
532
659
  events: ledger.events.length,
660
+ ...(o.real ? { real: o.real.status() } : {}),
533
661
  };
534
662
  };
535
663
  const loop = tapeTicks ? fromArray(tapeTicks) : o.source.ticks;
536
664
  for await (const k of loop) {
537
665
  now = k.t;
538
666
  const v = o.safety?.() ?? null;
539
- const haltWhy = v?.halt ?? (o.killSwitch?.() ? 'kill-switch' : null);
667
+ // task-1645: receipts land in the ledger as data; the real-mode kill switch halts like the panic file.
668
+ const rs = o.real?.step() ?? null;
669
+ for (const r of rs?.receipts ?? []) {
670
+ const dv = r.divergence;
671
+ ledger.add(now, 'receipt', { tag: 'real-signal', nonce: r.nonce, signalId: r.signalId, status: r.status, fill: r.fill, reason: r.reason, executor: r.executor, ...(dv ? { divergence: dv } : {}) });
672
+ }
673
+ const haltWhy = v?.halt ?? (o.killSwitch?.() ? 'kill-switch' : null) ?? rs?.halt ?? null;
540
674
  if (!killed && haltWhy) {
541
675
  killed = true;
542
676
  haltReason = haltWhy;
@@ -579,6 +713,7 @@ export async function runAutotrade(o) {
579
713
  ledger.add(now, 'halt', { reason: book.state.halted });
580
714
  o.approvals?.cancelAll(`halted:${book.state.halted}`);
581
715
  }
716
+ await sweepOpen(k);
582
717
  // Held coin → the monitor (mechanical first, then wakes).
583
718
  const pos = book.open.get(k.mint);
584
719
  const chatSell = o.approvals?.takeExit?.(k.mint) ?? null;
@@ -608,8 +743,15 @@ export async function runAutotrade(o) {
608
743
  }
609
744
  }
610
745
  }
746
+ else if (pos && drained.has(pos.id) && o.drain?.verdict(pos.id)) {
747
+ // task-1748: still drained per the pool probe: nothing sells into an emptied pool (the write-off closes it).
748
+ }
611
749
  else if (pos) {
612
- const prof = s.exits.profiles[pos.profileId];
750
+ drained.delete(pos.id);
751
+ const prof0 = s.exits.profiles[pos.profileId];
752
+ // task-1654: a real run's (or a paper scope's) maxHoldMinutes limit tightens the profile's holding cap (never loosens it).
753
+ const holdCap = o.real?.maxHoldMinutes?.() ?? o.maxHoldMinutes?.() ?? null;
754
+ const prof = holdCap !== null && (prof0.maxHoldMinutes == null || holdCap < prof0.maxHoldMinutes) ? { ...prof0, maxHoldMinutes: holdCap } : prof0;
613
755
  const acts = monitorTick(pos, prof, k, { minExitLiquidityUsd: x.minExitLiquidityUsd, sellUsd: (tok) => { const f = simSell(k, tok, x, 'estimate'); return 'refused' in f ? 0 : f.usd; } });
614
756
  const wakes = [];
615
757
  for (const a of acts) {
@@ -666,18 +808,26 @@ export async function runAutotrade(o) {
666
808
  if (sr.pass) {
667
809
  // Cheap pre-check so a full book does not spawn sessions it could never act on.
668
810
  const pre = clampEntry(book, k, {}, killed);
669
- const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k) : null;
811
+ const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k, vetCtx(k, pre.usd, h)) : null;
670
812
  if (!pre.ok) {
671
813
  if (!['already-open'].includes(pre.refused))
672
814
  count(`precheck:${pre.refused}`);
673
815
  }
674
816
  else if (v && !v.ok) {
675
- refuse(v.rule, { mint: k.mint, symbol: k.symbol, stage: 'vet', detail: v.detail });
676
- skippedUntil.set(k.mint, Infinity);
677
- say(`SKIP ${k.symbol} (${k.mint.slice(0, 4)}…): ${v.rule} (${v.detail})`);
817
+ // task-1748: a time gate (pool age, the second liquidity read) looks again later; any other refusal is for good.
818
+ const retry = v.retryAfterMs !== undefined;
819
+ refuse(v.rule, { mint: k.mint, symbol: k.symbol, stage: 'vet', detail: v.detail, ...(retry ? { retryAfterMs: v.retryAfterMs } : {}) });
820
+ skippedUntil.set(k.mint, retry ? now + v.retryAfterMs : Infinity);
821
+ say(`SKIP ${k.symbol} (${k.mint.slice(0, 4)}…): ${v.rule} (${v.detail})${retry ? `; again in ${Math.round(v.retryAfterMs / 1000)} s` : ''}`);
678
822
  }
679
823
  else if (decideAuto) {
680
824
  const cand = newCand(k, sr.matched, null, 'candidate', null);
825
+ if (v?.ok) {
826
+ if (v.sizeCapPct !== undefined)
827
+ cand.sizeCapPct = v.sizeCapPct;
828
+ if (v.pool)
829
+ cand.pool = v.pool;
830
+ }
681
831
  cands.set(k.mint, cand);
682
832
  ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
683
833
  await apply(cand, { action: 'enter', reason: 'auto' });
@@ -693,6 +843,12 @@ export async function runAutotrade(o) {
693
843
  const session = o.sessions(sid, s, { kind: 'candidate', mint: k.mint });
694
844
  gate.opened('candidate');
695
845
  const cand = newCand(k, sr.matched, session, 'candidate', null);
846
+ if (v?.ok) {
847
+ if (v.sizeCapPct !== undefined)
848
+ cand.sizeCapPct = v.sizeCapPct;
849
+ if (v.pool)
850
+ cand.pool = v.pool;
851
+ }
696
852
  cands.set(k.mint, cand);
697
853
  ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
698
854
  ledger.add(now, 'session_start', { sessionId: sid, sessionKind: 'candidate', agent: session.kind, mint: k.mint, model: sessionModel(session, s), budgetUsd: gate.caps.budgetUsdPerSession });
@@ -746,6 +902,14 @@ export async function runAutotrade(o) {
746
902
  while (pumps.size)
747
903
  await Promise.allSettled([...pumps]);
748
904
  const openAtEnd = [...book.open.values()].map((p) => ({ mint: p.mint, symbol: p.symbol, markUsd: round2(liquidationMark(p)), costUsd: p.costUsd, stuck: p.stuck }));
905
+ if (o.real) {
906
+ // The dead-man's graceful half: the executor hears the run is over (its outstanding nonces may be cancelled).
907
+ const why = killed ? `halt:${haltReason ?? 'kill'}` : o.tape ? 'end-of-tape' : 'runner-stopped';
908
+ // r2: what is still open is carried (held.json): the next real run of this strategy adopts it and keeps its exits.
909
+ o.real.carry([...book.open.values()]);
910
+ o.real.sendHalted(why);
911
+ ledger.add(now, 'real_halted', { tag: 'real-signal', reason: why, openAtEnd: openAtEnd.length });
912
+ }
749
913
  ledger.add(now, 'run_end', { trades: book.closed.length, openAtEnd: openAtEnd.length, cashUsd: book.state.cashUsd, markedEquityUsd: book.markedEquity(), sessionCostUsd: sessionCost, sessions: sessionsOpened, positionSessions: positionSessionsOpened, head: ledger.head() });
750
914
  publishFocus();
751
915
  if (o.onState)
@@ -772,6 +936,8 @@ export function exitPlanLine(p) {
772
936
  return `${p.id}: ${parts.join(', ')}`;
773
937
  }
774
938
  const round4 = (v) => Math.round(v * 1e4) / 1e4;
939
+ /** task-1645: a fill that went out as a trade-fire signal: the ledger row is tagged and names the signal. */
940
+ const sigTag = (f) => { const x = f; const g = x.signal; return g ? { tag: 'real-signal', nonce: g.nonce, signalId: g.id, ...(x.limitClamps?.length ? { limitClamps: x.limitClamps } : {}) } : {}; };
775
941
  const fillView = (f) => ({ side: f.side, usd: f.usd, tokens: Number(f.tokens.toPrecision(8)), price: f.price, avgPrice: f.avgPrice, impactPct: f.impactPct, partial: f.partial, reason: f.reason });
776
942
  /** Infinity (no runner cap) is written as null so the ledger stays JSON. */
777
943
  /** The model a session actually runs (a run's --model override wins over the strategy's), for the ledger. */
@@ -0,0 +1,125 @@
1
+ // dexterm autotrade — the always-on service's guard aggregator (0.2.0 card B, AUTOTRADE-SPEC §7.3 / §7.4).
2
+ //
3
+ // Every 10 s (and on demand) the service folds what it can see into named guard states. Each state has ONE effect:
4
+ // stop the service stops the fleet (a soft stop: no new entries, sessions closed, exits keep running)
5
+ // pause-entries no new entries while it holds; lifts by itself (hysteresis where a value can flap)
6
+ // notice shown in `service status` and sent once as a notification; nothing pauses
7
+ // Open positions are never paused by any state: their exits keep running (the monitor is never paused, §7.4).
8
+ //
9
+ // state id effect from
10
+ // stop-file stop the service STOP marker (`service stop`, a plain `touch`)
11
+ // panic stop the panic file (`dexterm wallet panic`); the fleet halts itself too
12
+ // disk-low pause-entries free space under runner.json disk.minFreeGb (default 5 GB); +1 GB to lift
13
+ // load-high pause-entries 1-min load above load.pauseAboveLoad1m for load.sustainSec; lifts below resumeBelowLoad1m
14
+ // busy-process pause-entries a runner.json load.busyProcs pattern matches a running process (not ours)
15
+ // data-stale pause-entries no strategy has seen a market tick for dataStaleSec (default 300 s)
16
+ // runner-stale notice the fleet's heartbeat (runner-status.json) is older than 30 s
17
+ // allowance-high notice the model usage windows are above runner.json allowanceGuard (the fleet pauses sessions itself)
18
+ // day-cap notice the fleet's global day caps are used up (sessions resume when the day rolls)
19
+ // recorder-down notice the market recorder is installed but has written nothing for recorderStaleSec
20
+ // (default 15 min); not installed → notice with `dexterm record install`
21
+ // strategy-halted notice a strategy halted itself (daily loss / drawdown caps); one per strategy
22
+ //
23
+ // The guard is pure over its inputs (the service collects them); transitions come back so the caller can log one
24
+ // ledger line and send one notification per change, never one per tick.
25
+ export const DEFAULT_GUARD_CONFIG = {
26
+ minFreeGb: 5, pauseAboveLoad1m: 25, resumeBelowLoad1m: 20, sustainSec: 120, busyProcs: [],
27
+ dataStaleSec: 300, recorderStaleSec: 900, fiveHourMax: 0.6, sevenDayMax: 0.8,
28
+ };
29
+ /** runner.json → the guard's knobs (the stricter of the two never matters here: these only pause). */
30
+ export function guardConfigFrom(rc, over = {}) {
31
+ return {
32
+ ...DEFAULT_GUARD_CONFIG,
33
+ ...(rc?.load ? { pauseAboveLoad1m: rc.load.pauseAboveLoad1m, resumeBelowLoad1m: rc.load.resumeBelowLoad1m, sustainSec: rc.load.sustainSec, busyProcs: [...rc.load.busyProcs] } : {}),
34
+ ...(rc?.disk ? { minFreeGb: rc.disk.minFreeGb } : {}),
35
+ ...(rc?.allowanceGuard ? { fiveHourMax: rc.allowanceGuard.fiveHourMax, sevenDayMax: rc.allowanceGuard.sevenDayMax } : {}),
36
+ ...over,
37
+ };
38
+ }
39
+ const gb = (n) => `${n.toFixed(1)} GB`;
40
+ export class ServiceGuard {
41
+ cfg;
42
+ active = new Map();
43
+ loadAboveSince = null;
44
+ loadPaused = false;
45
+ diskLow = false;
46
+ constructor(cfg = DEFAULT_GUARD_CONFIG) { this.cfg = cfg; }
47
+ evaluate(i) {
48
+ const c = this.cfg;
49
+ const want = new Map();
50
+ const on = (id, effect, detail) => { want.set(id, { effect, detail }); };
51
+ if (i.stopFile)
52
+ on('stop-file', 'stop', i.stopFile);
53
+ if (i.panic)
54
+ on('panic', 'stop', `the panic switch is engaged (${i.panic}); \`dexterm wallet resume\` lifts it`);
55
+ if (i.freeGb !== null) {
56
+ if (!this.diskLow && i.freeGb < c.minFreeGb)
57
+ this.diskLow = true;
58
+ else if (this.diskLow && i.freeGb >= c.minFreeGb + 1)
59
+ this.diskLow = false;
60
+ if (this.diskLow)
61
+ on('disk-low', 'pause-entries', `${gb(i.freeGb)} free, under the ${gb(c.minFreeGb)} floor (runner.json disk.minFreeGb); lifts at ${gb(c.minFreeGb + 1)}`);
62
+ }
63
+ if (i.load1m !== null) {
64
+ if (i.load1m > c.pauseAboveLoad1m) {
65
+ this.loadAboveSince ??= i.now;
66
+ if (i.now - this.loadAboveSince >= c.sustainSec * 1000)
67
+ this.loadPaused = true;
68
+ }
69
+ else {
70
+ this.loadAboveSince = null;
71
+ if (i.load1m < c.resumeBelowLoad1m)
72
+ this.loadPaused = false;
73
+ }
74
+ if (this.loadPaused)
75
+ on('load-high', 'pause-entries', `1-min load ${i.load1m.toFixed(1)} (pauses above ${c.pauseAboveLoad1m} for ${c.sustainSec} s, resumes below ${c.resumeBelowLoad1m})`);
76
+ }
77
+ if (i.busy.length)
78
+ on('busy-process', 'pause-entries', `a busy process is running (${i.busy.slice(0, 3).join(', ')})`);
79
+ if (i.upMs >= c.dataStaleSec * 1000 && (i.dataAgeMs === null || i.dataAgeMs > c.dataStaleSec * 1000)) {
80
+ on('data-stale', 'pause-entries', i.dataAgeMs === null ? `no market tick since the start (${Math.round(i.upMs / 1000)} s)` : `the newest market tick is ${Math.round(i.dataAgeMs / 1000)} s old (limit ${c.dataStaleSec} s)`);
81
+ }
82
+ if (i.runnerAgeMs !== null && i.runnerAgeMs > 30_000)
83
+ on('runner-stale', 'notice', `the strategies' heartbeat is ${Math.round(i.runnerAgeMs / 1000)} s old`);
84
+ const a = i.allowance;
85
+ if (a && ((a.fiveHour ?? 0) > c.fiveHourMax || (a.sevenDay ?? 0) > c.sevenDayMax)) {
86
+ on('allowance-high', 'notice', `model usage 5 h ${pctOf(a.fiveHour)} / 7 d ${pctOf(a.sevenDay)} (limits ${pctOf(c.fiveHourMax)} / ${pctOf(c.sevenDayMax)}): no new sessions until it falls`);
87
+ }
88
+ if (i.dayCap)
89
+ on('day-cap', 'notice', `${i.dayCap}: no new sessions until the day rolls`);
90
+ if (i.recorder) {
91
+ if (!i.recorder.installed)
92
+ on('recorder-down', 'notice', 'the market recorder is not installed: backtests and tuning see no new history (`dexterm record install`)');
93
+ else if (i.recorder.newestAgeMs === null || i.recorder.newestAgeMs > c.recorderStaleSec * 1000)
94
+ on('recorder-down', 'notice', `the market recorder has written nothing for ${i.recorder.newestAgeMs === null ? 'ever' : `${Math.round(i.recorder.newestAgeMs / 60_000)} min`} (\`dexterm record status\`)`);
95
+ }
96
+ for (const [id, why] of Object.entries(i.halted))
97
+ on(`strategy-halted:${id}`, 'notice', `strategy ${id} halted itself (${why}); its open positions keep their exits`);
98
+ const transitions = [];
99
+ for (const [id, w] of want) {
100
+ const cur = this.active.get(id);
101
+ if (!cur) {
102
+ this.active.set(id, { id, effect: w.effect, detail: w.detail, since: i.now });
103
+ transitions.push({ id, on: true, effect: w.effect, detail: w.detail });
104
+ }
105
+ else
106
+ cur.detail = w.detail;
107
+ }
108
+ for (const [id, cur] of [...this.active]) {
109
+ if (want.has(id))
110
+ continue;
111
+ this.active.delete(id);
112
+ transitions.push({ id, on: false, effect: cur.effect, detail: cur.detail });
113
+ }
114
+ const states = [...this.active.values()].map((s) => ({ ...s }));
115
+ const stop = states.find((s) => s.effect === 'stop')?.id ?? null;
116
+ const pause = states.find((s) => s.effect === 'pause-entries')?.id ?? null;
117
+ return { states, stop, pauseEntries: pause ? `guard:${pause}` : null, transitions };
118
+ }
119
+ }
120
+ const pctOf = (v) => (v === null ? '?' : `${Math.round(v * 100)}%`);
121
+ /** A one-line notification text for a guard transition (plain text, no paths, no secrets). */
122
+ export function guardNoticeText(t) {
123
+ const what = t.effect === 'stop' ? 'STOP' : t.effect === 'pause-entries' ? 'ENTRIES PAUSED' : 'NOTICE';
124
+ return t.on ? `autotrade service · ${what} · ${t.id}: ${t.detail}` : `autotrade service · cleared · ${t.id}${t.effect === 'pause-entries' ? ': entries resume' : ''}`;
125
+ }