dexterm-cli 0.1.6 → 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 (395) hide show
  1. package/dist/packages/agents/src/schema.js +22 -1
  2. package/dist/packages/agents/src/spectator.js +3 -1
  3. package/dist/packages/agents/src/story.js +11 -2
  4. package/dist/packages/agents/src/tiers.js +36 -2
  5. package/dist/packages/agents/src/tools.js +35 -2
  6. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
  7. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  8. package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
  9. package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
  10. package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
  11. package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
  12. package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
  13. package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
  14. package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
  15. package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
  16. package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
  17. package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
  18. package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
  19. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  20. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  21. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  22. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  23. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  24. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  25. package/dist/packages/autotrade/src/account.js +74 -0
  26. package/dist/packages/autotrade/src/backtest.js +358 -0
  27. package/dist/packages/autotrade/src/basis-text.js +32 -0
  28. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  29. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  30. package/dist/packages/autotrade/src/chat/config.js +95 -0
  31. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  32. package/dist/packages/autotrade/src/chat/control.js +104 -0
  33. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  34. package/dist/packages/autotrade/src/chat/format.js +71 -0
  35. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  36. package/dist/packages/autotrade/src/chat/index.js +18 -0
  37. package/dist/packages/autotrade/src/chat/notify.js +302 -0
  38. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  39. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  40. package/dist/packages/autotrade/src/chat/secrets.js +106 -0
  41. package/dist/packages/autotrade/src/chat/service.js +251 -0
  42. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  43. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  44. package/dist/packages/autotrade/src/engine.js +401 -0
  45. package/dist/packages/autotrade/src/execution-port.js +97 -0
  46. package/dist/packages/autotrade/src/executor-health.js +217 -0
  47. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  48. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  49. package/dist/packages/autotrade/src/fleet.js +642 -0
  50. package/dist/packages/autotrade/src/forward.js +355 -0
  51. package/dist/packages/autotrade/src/index.js +72 -0
  52. package/dist/packages/autotrade/src/journey.js +477 -0
  53. package/dist/packages/autotrade/src/ledger.js +108 -0
  54. package/dist/packages/autotrade/src/limits.js +682 -0
  55. package/dist/packages/autotrade/src/live-source.js +182 -0
  56. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  57. package/dist/packages/autotrade/src/monitor.js +190 -0
  58. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  59. package/dist/packages/autotrade/src/outbox.js +394 -0
  60. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  61. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  62. package/dist/packages/autotrade/src/real-mode.js +688 -0
  63. package/dist/packages/autotrade/src/receipts.js +330 -0
  64. package/dist/packages/autotrade/src/replay.js +198 -0
  65. package/dist/packages/autotrade/src/report.js +168 -0
  66. package/dist/packages/autotrade/src/runner-config.js +99 -0
  67. package/dist/packages/autotrade/src/runner.js +923 -0
  68. package/dist/packages/autotrade/src/safety.js +243 -0
  69. package/dist/packages/autotrade/src/service-guard.js +125 -0
  70. package/dist/packages/autotrade/src/service-install.js +213 -0
  71. package/dist/packages/autotrade/src/service-recover.js +232 -0
  72. package/dist/packages/autotrade/src/service.js +289 -0
  73. package/dist/packages/autotrade/src/session.js +269 -0
  74. package/dist/packages/autotrade/src/sessions.js +503 -0
  75. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  76. package/dist/packages/autotrade/src/signal.js +196 -0
  77. package/dist/packages/autotrade/src/store.js +250 -0
  78. package/dist/packages/autotrade/src/strategy.js +258 -0
  79. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  80. package/dist/packages/autotrade/src/yolo.js +140 -0
  81. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  82. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  83. package/dist/packages/cli/src/ai-cmd.js +412 -0
  84. package/dist/packages/cli/src/analyst/backend.js +83 -15
  85. package/dist/packages/cli/src/analyst/codex.js +15 -9
  86. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  87. package/dist/packages/cli/src/analyst/session.js +5 -2
  88. package/dist/packages/cli/src/app-help-gen.js +56 -8
  89. package/dist/packages/cli/src/autotrade-cmd/audit.js +74 -0
  90. package/dist/packages/cli/src/autotrade-cmd/backtest.js +216 -0
  91. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  92. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  93. package/dist/packages/cli/src/autotrade-cmd/fleet.js +401 -0
  94. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  95. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  96. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  97. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  98. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  99. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  100. package/dist/packages/cli/src/autotrade-cmd/run.js +657 -0
  101. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  102. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  103. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  104. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  105. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  106. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  107. package/dist/packages/cli/src/autotrade-cmd.js +214 -0
  108. package/dist/packages/cli/src/display.js +156 -17
  109. package/dist/packages/cli/src/doctor.js +62 -3
  110. package/dist/packages/cli/src/export.js +10 -2
  111. package/dist/packages/cli/src/index.js +1 -1
  112. package/dist/packages/cli/src/ledger-cmd.js +290 -0
  113. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  114. package/dist/packages/cli/src/live/index.js +23 -89
  115. package/dist/packages/cli/src/live/screens/token-live.js +9 -6
  116. package/dist/packages/cli/src/live/trade.js +12 -457
  117. package/dist/packages/cli/src/live/watchlist.js +1 -1
  118. package/dist/packages/cli/src/main.js +147 -190
  119. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  120. package/dist/packages/cli/src/plan-cmd.js +1 -1
  121. package/dist/packages/cli/src/pnl-app.js +31 -21
  122. package/dist/packages/cli/src/pnl-card.js +162 -21
  123. package/dist/packages/cli/src/popup.js +22 -6
  124. package/dist/packages/cli/src/record-cmd.js +141 -20
  125. package/dist/packages/cli/src/screens/agent-hud.js +34 -2
  126. package/dist/packages/cli/src/screens/analyst-session.js +7 -4
  127. package/dist/packages/cli/src/screens/analyst.js +3 -2
  128. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  129. package/dist/packages/cli/src/screens/autotrade-keys.js +182 -0
  130. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  131. package/dist/packages/cli/src/screens/autotrade-screen.js +776 -0
  132. package/dist/packages/cli/src/screens/autotrade-ui.js +650 -0
  133. package/dist/packages/cli/src/screens/autotrade-worker.js +91 -0
  134. package/dist/packages/cli/src/screens/brain.js +37 -11
  135. package/dist/packages/cli/src/screens/overlay.js +5 -2
  136. package/dist/packages/cli/src/screens/plans.js +5 -17
  137. package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
  138. package/dist/packages/cli/src/screens/positions.js +6 -28
  139. package/dist/packages/cli/src/screens/ticket.js +38 -205
  140. package/dist/packages/cli/src/screens/token.js +3 -3
  141. package/dist/packages/cli/src/state.js +25 -19
  142. package/dist/packages/cli/src/telegram-cmd.js +454 -0
  143. package/dist/packages/cli/src/trade.js +3 -128
  144. package/dist/packages/cli/src/version.js +1 -1
  145. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  146. package/dist/packages/cli/src/wallet-command.js +8 -11
  147. package/dist/packages/cli/src/widgets.js +0 -12
  148. package/dist/packages/custom/src/config.js +20 -2
  149. package/dist/packages/custom/src/schema.js +35 -10
  150. package/dist/packages/custom/src/workspace.js +3 -3
  151. package/dist/packages/data/src/basis.js +191 -0
  152. package/dist/packages/data/src/detail.js +53 -2
  153. package/dist/packages/data/src/index.js +0 -14
  154. package/dist/packages/data/src/store.js +14 -5
  155. package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
  156. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  157. package/dist/packages/data/src/trade/bridge.js +4 -102
  158. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  159. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  160. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  161. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  162. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  163. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  164. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  165. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  166. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  167. package/dist/packages/data/src/trade/plans/index.js +0 -1
  168. package/dist/packages/data/src/trade/plans/service.js +3 -27
  169. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  170. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  171. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  172. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  173. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  174. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  175. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  176. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  177. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  178. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  179. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  180. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  181. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  182. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  183. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  184. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  185. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  186. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +229 -0
  187. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  188. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  189. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  190. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  191. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  192. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  193. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  194. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  195. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  196. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
  198. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  200. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  201. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +51 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +38 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +37 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +50 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  231. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  232. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  233. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  235. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  236. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  237. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  240. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  242. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
  243. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  244. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  246. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
  247. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  248. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  249. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  250. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  251. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  254. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  255. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  256. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  257. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +31 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  271. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  273. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  274. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  275. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  277. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  278. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  281. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  282. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  284. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  288. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  289. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
  290. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  291. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
  292. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  293. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  295. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  297. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  298. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  299. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
  301. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  302. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  304. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  305. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  306. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
  307. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  308. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  309. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  310. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  311. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  312. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  313. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
  314. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  315. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  316. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  317. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  318. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  319. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  320. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  321. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  322. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  323. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  324. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  325. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  326. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  327. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  328. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  329. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  330. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  331. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  332. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  333. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  334. package/dist/packages/mcp/assets/kb/entries/autotrade.md +40 -0
  335. package/dist/packages/mcp/src/app-help.js +1 -1
  336. package/dist/packages/mcp/src/autotrade-tools.js +1107 -0
  337. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  338. package/dist/packages/mcp/src/engine/config.js +159 -0
  339. package/dist/packages/mcp/src/engine/index.js +10 -0
  340. package/dist/packages/mcp/src/engine/loop.js +101 -0
  341. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  342. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  343. package/dist/packages/mcp/src/engine/probe.js +110 -0
  344. package/dist/packages/mcp/src/engine/reason.js +35 -0
  345. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  346. package/dist/packages/mcp/src/index.js +7 -1
  347. package/dist/packages/mcp/src/ledger-tools.js +494 -0
  348. package/dist/packages/mcp/src/paper-agent.js +2 -0
  349. package/dist/packages/mcp/src/server.js +138 -19
  350. package/dist/packages/packs/src/backtest.js +3 -1
  351. package/dist/packages/popup/src/pages/chart-lib.js +134 -21
  352. package/dist/packages/popup/src/pages/chart.css +9 -0
  353. package/dist/packages/popup/src/pages/chart.html +3 -1
  354. package/dist/packages/popup/src/pages/chart.js +104 -7
  355. package/dist/packages/popup/src/protocol.js +2 -0
  356. package/dist/packages/popup/src/server.js +4 -0
  357. package/dist/packages/recorder/src/index.js +2 -0
  358. package/dist/packages/recorder/src/recorder.js +63 -6
  359. package/dist/packages/recorder/src/retention.js +100 -0
  360. package/dist/packages/recorder/src/schema.js +34 -1
  361. package/dist/packages/recorder/src/ticks.js +237 -0
  362. package/dist/packages/tui/src/index.js +1 -0
  363. package/dist/packages/tui/src/keymap.js +28 -1
  364. package/dist/packages/tui/src/markers.js +219 -0
  365. package/dist/packages/tui/src/widgets.js +149 -2
  366. package/package.json +2 -1
  367. package/dist/packages/data/src/trade/caps.js +0 -323
  368. package/dist/packages/data/src/trade/executor.js +0 -933
  369. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  370. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  371. package/dist/packages/data/src/trade/signer/client.js +0 -197
  372. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  373. package/dist/packages/data/src/trade/signer/host.js +0 -97
  374. package/dist/packages/data/src/trade/signer/main.js +0 -121
  375. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  376. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  377. package/dist/packages/data/src/trade/signer/service.js +0 -400
  378. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  379. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  380. package/dist/packages/data/src/trade/swap-build.js +0 -108
  381. package/dist/packages/data/src/trade/token-delta.js +0 -201
  382. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  383. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  384. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  385. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  386. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  387. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  388. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  389. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  390. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  391. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  392. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  393. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  394. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  395. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -0,0 +1,923 @@
1
+ // dexterm autotrade — the run loop (task-1498 spike, task-1505 AT-3; AUTOTRADE-SPEC §4–§6).
2
+ //
3
+ // tick → (held coin) monitor: mechanical exits at once, WAKE events to the position's session (or, with
4
+ // agent.respawnOnWake, a one-answer position session when the coin has none)
5
+ // → (candidate) delta to its session (coalesced to the strategy cadence, urgent moves at once)
6
+ // → (other coin) screener: entry rules → precheck clamp → candidate → session with the full dossier, or auto entry
7
+ // session reply → validated DECISION → clamp (strategy bounds) → paper order → fill after the latency.
8
+ //
9
+ // Ticks come from a recorded tape (backtests, `--replay`) or a live source (`dexterm autotrade run --paper`, the
10
+ // Store fed from the keyless data layer). Pacing: lockstep awaits every reply before the next tick (deterministic
11
+ // backtests); paced replays sleep per tick while a session is live and let replies land when they land; live runs
12
+ // never sleep (the source paces itself) and replies land when they land.
13
+ //
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, 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
17
+ // sessions' day caps and allowance guard live in SessionGate (sessions.ts); with none given, the strategy's own
18
+ // limits apply; the gate counts real usage per wall-clock day, so a fast --replay over many tape days never resets
19
+ // the $/day cap. The host part at the end of this file (lock, rendezvous, heartbeat) is what the CLI uses.
20
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeSync } from 'node:fs';
21
+ import { uptime } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { Ledger, writeJsonAtomic } from "./ledger.js";
24
+ import { EntryRules, SimBook, clampEntry, simBuy, simSell } from "./engine.js";
25
+ import { monitorTick } from "./monitor.js";
26
+ import { buildDelta, renderDelta, renderEvent, urgent, view } from "./session.js";
27
+ import { SessionGate, renderDossierRecord, renderPositionDossier, tapeDossierRecord } from "./sessions.js";
28
+ import { DEFAULT_RUNNER_CONFIG, effectiveCaps } from "./runner-config.js";
29
+ import { DEFAULT_PRICE_BASIS, fillMcaps, levelText } from "./basis-text.js";
30
+ export { Ledger } from "./ledger.js";
31
+ export function simExecutor(x) {
32
+ return { name: 'sim', buy: (k, usd) => simBuy(k, usd, x), sell: (k, tokens, reason) => simSell(k, tokens, x, reason) };
33
+ }
34
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
35
+ const REVIEW = ['time_review', 'time_tighten'];
36
+ const INFO_ONLY = ['mechanical_exit', 'stuck'];
37
+ async function* fromArray(ticks) { for (const k of ticks)
38
+ yield k; }
39
+ export async function runAutotrade(o) {
40
+ const s = o.strategy;
41
+ // task-1571: autotrade runs on paper only. dexterm holds no key and places no live order; live trading happens in the
42
+ // Telegram trading bot of the user's choice.
43
+ // task-1645: mode "real" = the same paper engine, plus a LIVE TRADE FIRE signal per fill for the user's own executor;
44
+ // it needs the real-mode hook (armed on the user's terminal, checked by the host) and never runs without it.
45
+ if (s.mode === 'real' && !o.real)
46
+ 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)`);
47
+ if (s.mode !== 'paper' && s.mode !== 'real')
48
+ 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`);
49
+ if (o.real && s.mode !== 'real')
50
+ throw new Error(`strategy ${s.id} is mode "${s.mode}": the real-mode hook is for a strategy in mode "real" only`);
51
+ if (o.real && (!o.executor || o.executor !== o.real))
52
+ throw new Error('real mode: the executor must be the real-mode signal executor itself');
53
+ if (!o.tape && !o.source)
54
+ throw new Error('runAutotrade needs a tape or a live source');
55
+ const ledger = o.ledger ?? new Ledger();
56
+ const say = o.onLine ?? (() => { });
57
+ const tapeTicks = o.tape?.ticks;
58
+ const t0 = tapeTicks ? (tapeTicks[0]?.t ?? 0) : (o.source.startT ?? Date.now());
59
+ if (o.account && s.mode !== 'paper')
60
+ throw new Error(`strategy ${s.id}: a paper account holds simulated cash only; it never backs a live run`);
61
+ const book = new SimBook(s, t0, o.account, o.member);
62
+ const rules = new EntryRules(s);
63
+ const pacing = o.pacing ?? { mode: 'lockstep' };
64
+ const decideAuto = o.sessions === null || s.entry.decide === 'auto';
65
+ const x = s.execution;
66
+ const exec = o.executor ?? simExecutor(x);
67
+ // Without a runner config the strategy's own limits apply (no extra runner caps).
68
+ const gate = o.gate ?? new SessionGate(effectiveCaps(s, { ...DEFAULT_RUNNER_CONFIG, caps: { candidateSessionsPerDay: Infinity, positionSessionsPerDay: Infinity, modelUsdPerDay: Infinity, turnsPerDay: Infinity } }), { fiveHourMax: 1, sevenDayMax: 1, onLimit: 'pause-until-reset' });
69
+ const basis = o.basis ?? DEFAULT_PRICE_BASIS;
70
+ const lvl = (price, k) => levelText(basis, price, k);
71
+ const dossierFn = o.dossier ?? ((k, h, matched, b) => renderDossierRecord(tapeDossierRecord(k), { mint: k.mint, symbol: k.symbol, matched }, k, h, b, basis));
72
+ const latest = new Map();
73
+ const hist = new Map();
74
+ const cands = new Map();
75
+ const byPosition = new Map();
76
+ const skippedUntil = new Map();
77
+ /** Coins the vet passed this run (a refused one goes to skippedUntil for good). */
78
+ const vetted = new Set();
79
+ const orders = [];
80
+ const refusals = {};
81
+ const clampCount = {};
82
+ const curve = [];
83
+ let sessionsOpened = 0;
84
+ let positionSessionsOpened = 0;
85
+ let sessionCost = 0;
86
+ let maxDd = 0;
87
+ let now = t0;
88
+ let killed = false;
89
+ let haltReason = null;
90
+ let flattening = false;
91
+ let endWhenFlat = false;
92
+ let entriesPaused = null;
93
+ let focusSig = '';
94
+ /** Every reply pump still running, including those of closed candidates (their late replies are logged before run_end). */
95
+ const pumps = new Set();
96
+ /** Proposals waiting for an answer, by mint (task-1527). */
97
+ const approvalsOpen = new Map();
98
+ 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 } : {}) });
99
+ // task-1645 r2: a real run takes over the real positions an earlier run of this strategy left open (held.json),
100
+ // so their exits keep firing. A profile the strategy no longer has falls back to its default profile.
101
+ for (const p of o.real?.inherited() ?? []) {
102
+ const from = p.profileId;
103
+ if (!s.exits.profiles[p.profileId])
104
+ p.profileId = s.exits.default;
105
+ if (!book.adopt(p))
106
+ continue;
107
+ 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 } });
108
+ 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`);
109
+ }
110
+ // 0.2.0 card B: positions a crashed run left open. A profile the (re-read) strategy no longer has falls back to
111
+ // its default profile, so every adopted position has exits.
112
+ for (const p of o.adopt ?? []) {
113
+ const from = p.profileId;
114
+ if (!s.exits.profiles[p.profileId])
115
+ p.profileId = s.exits.default;
116
+ if (!book.adopt(p))
117
+ continue;
118
+ ledger.add(t0, 'position_adopted', { positionId: p.id, mint: p.mint, symbol: p.symbol, ...(from !== p.profileId ? { profileFrom: from } : {}), position: { ...p, fills: p.fills } });
119
+ say(`RESUMED ${p.symbol} (${p.mint.slice(0, 4)}…): ${p.profileId}, cost $${(p.costUsd - p.realizedUsd).toFixed(2)} still open; its exits keep running`);
120
+ }
121
+ const count = (rule) => { refusals[rule] = (refusals[rule] ?? 0) + 1; };
122
+ const refuse = (rule, data) => { count(rule); ledger.add(now, 'refused', { rule, ...data }); };
123
+ // A vet that throws or hangs is a check that could not run: the strategy's `unknown` decides (reject by default).
124
+ const vetCoin = async (k) => {
125
+ let r;
126
+ try {
127
+ r = await o.vet(k);
128
+ }
129
+ catch (e) {
130
+ r = { ok: false, rule: 'vet-error', detail: e.message.slice(0, 160) };
131
+ }
132
+ if (r.ok) {
133
+ vetted.add(k.mint);
134
+ if (r.note)
135
+ ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200) });
136
+ }
137
+ return r;
138
+ };
139
+ const liveCandidateSessions = () => [...cands.values()].filter((q) => q.session && !q.positionId).length;
140
+ const everyLive = () => [...new Set([...cands.values(), ...byPosition.values()])];
141
+ const newCand = (k, matched, session, kind, positionId) => ({
142
+ mint: k.mint, symbol: k.symbol, since: now, lastMatch: now, matched, session, sessionKind: kind, lastSent: view(k), lastSentT: now, seq: 0,
143
+ inflight: null, pendingText: [], notes: [], closing: false, positionId, awaitingReview: null, oneShot: kind === 'position', costSeen: 0, turns: 0, lastDecision: null,
144
+ });
145
+ const closeCandidate = async (c, reason) => {
146
+ if (c.closing)
147
+ return;
148
+ c.closing = true;
149
+ if (cands.get(c.mint) === c)
150
+ cands.delete(c.mint);
151
+ if (c.positionId) {
152
+ if (byPosition.get(c.positionId) === c)
153
+ byPosition.delete(c.positionId);
154
+ }
155
+ else
156
+ skippedUntil.set(c.mint, now + Math.max(s.entry.candidateTtlSec * 1000, s.risk.perCoin.cooldownMin * 60e3));
157
+ if (c.session) {
158
+ // Never await c.inflight here: a close can come from inside the reply pump itself. Closing the
159
+ // session settles a pending send with null.
160
+ const st = await c.session.close(reason);
161
+ if (st.costUsd > c.costSeen) {
162
+ gate.spend(st.costUsd - c.costSeen);
163
+ c.costSeen = st.costUsd;
164
+ }
165
+ sessionCost += st.costUsd;
166
+ ledger.add(now, 'session_end', { sessionId: c.session.id, sessionKind: c.sessionKind, mint: c.mint, reason, turns: st.turns, costUsd: st.costUsd });
167
+ say(`session ${c.session.id} ended (${reason}) · ${st.turns} turns · $${st.costUsd.toFixed(4)}`);
168
+ }
169
+ };
170
+ const sellPosition = async (p, k, tokens, reason, detail) => {
171
+ if (tokens <= 0)
172
+ return;
173
+ const f = await exec.sell(k, Math.min(tokens, p.tokens), reason);
174
+ if ('refused' in f) {
175
+ if (!p.stuck) {
176
+ p.stuck = true;
177
+ ledger.add(now, 'stuck', { positionId: p.id, mint: p.mint, why: f.refused, reason });
178
+ say(`STUCK ${p.symbol}: cannot sell (${f.refused})`);
179
+ }
180
+ return;
181
+ }
182
+ p.stuck = false;
183
+ ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), detail, ...sigTag(f) });
184
+ say(`SELL ${p.symbol} ${reason}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)} (impact ${f.impactPct}%) — ${detail}`);
185
+ const ct = book.sell(p, f);
186
+ if (ct) {
187
+ // task-1530: the price extremes the monitor saw while held (the trade ledger's MFE / MAE)
188
+ ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, ...(o.real ? { tag: 'real-signal' } : {}) });
189
+ say(`CLOSED ${ct.symbol}: ${ct.pnlUsd >= 0 ? '+' : ''}$${ct.pnlUsd.toFixed(2)} (${ct.pnlPct}%) by ${ct.exitReason}`);
190
+ }
191
+ };
192
+ /** Apply a validated decision at the current virtual time. */
193
+ const apply = async (c, d) => {
194
+ const k = latest.get(c.mint);
195
+ const pos = book.open.get(c.mint);
196
+ if (!pos || pos.id !== c.positionId) {
197
+ if (c.positionId) {
198
+ ledger.add(now, 'late_reply_ignored', { sessionId: c.session?.id ?? null, mint: c.mint, decision: d, why: 'position already closed' });
199
+ return;
200
+ }
201
+ // Candidate phase: enter / wait / skip.
202
+ if (d.action === 'skip') {
203
+ await closeCandidate(c, 'agent-skip');
204
+ return;
205
+ }
206
+ if (d.action !== 'enter')
207
+ return;
208
+ if (orders.some((q) => q.mint === c.mint))
209
+ return;
210
+ if (entriesPaused && !killed) {
211
+ refuse('entries-paused', { mint: c.mint, why: entriesPaused });
212
+ await closeCandidate(c, 'refused:entries-paused');
213
+ return;
214
+ }
215
+ const req = { profile: d.profile, sizePct: d.sizePct, sizeUsd: d.sizeUsd, confidence: d.confidence };
216
+ const r = clampEntry(book, k, req, killed);
217
+ 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 });
218
+ for (const cl of r.clamps)
219
+ clampCount[cl.rule] = (clampCount[cl.rule] ?? 0) + 1;
220
+ if (!r.ok) {
221
+ refuse(r.refused, { mint: c.mint, stage: 'entry' });
222
+ await closeCandidate(c, `refused:${r.refused}`);
223
+ return;
224
+ }
225
+ say(`clamp ${k.symbol}: asked ${d.sizePct !== undefined ? `${d.sizePct}%` : d.sizeUsd !== undefined ? `$${d.sizeUsd}` : 'default'} → $${r.usd} (${r.basis}${r.clamps.length ? `; clamped by ${r.clamps.map((q) => q.rule).join(', ')}` : ''})`);
226
+ const needsApproval = s.entry.decide === 'agent+approve' || (o.approvals?.required(s) ?? false);
227
+ if (needsApproval) {
228
+ await propose(c, k, d, r);
229
+ return;
230
+ }
231
+ const ladder = (s.sizing.ladder ?? []).slice(1).map((tr) => ({ frac: tr.frac, atPct: tr.atPct, deadline: now + tr.withinMin * 60e3, usd: r.usd * tr.frac }));
232
+ orders.push({ mint: c.mint, fillAt: now + x.latencyMs, decidedPrice: k.price ?? 0, usd: r.firstTrancheUsd, firstUsd: r.firstTrancheUsd, profile: r.profile.id, sessionId: c.session?.id ?? null, trigger: c.matched, ladder, kind: 'open' });
233
+ return;
234
+ }
235
+ // Position phase: hold / exit / tighten / reprofile. The agent may only reduce risk or move along profile edges.
236
+ if (d.action === 'hold' || d.action === 'wait')
237
+ return;
238
+ if (d.action === 'exit') {
239
+ const frac = Math.min(1, d.fraction ?? 1);
240
+ await sellPosition(pos, k, pos.tokens * frac, frac >= 0.999 ? 'agent-exit' : 'agent-trim', d.reason);
241
+ return;
242
+ }
243
+ if (d.action === 'tighten') {
244
+ const prof = s.exits.profiles[pos.profileId];
245
+ if (prof.trailingStop) {
246
+ pos.trailArmed = true;
247
+ ledger.add(now, 'tighten', { positionId: pos.id, armed: 'trail' });
248
+ }
249
+ else if (prof.bagTrail && pos.costRecovered) {
250
+ // A free bag tightens by halving its trail (never below 10%), once; it can never be loosened again.
251
+ pos.bagTrailPct = Math.max(10, Math.min(pos.bagTrailPct ?? prof.bagTrail.trailPct, Math.round(prof.bagTrail.trailPct / 2)));
252
+ ledger.add(now, 'tighten', { positionId: pos.id, armed: 'bag-trail', trailPct: pos.bagTrailPct });
253
+ }
254
+ else
255
+ refuse('tighten-no-trail', { positionId: pos.id });
256
+ return;
257
+ }
258
+ if (d.action === 'reprofile') {
259
+ const from = s.exits.profiles[pos.profileId];
260
+ const to = d.profile ? s.exits.profiles[d.profile] : undefined;
261
+ if (!to || !from.reprofileTo.includes(to.id)) {
262
+ refuse('reprofile-edge', { positionId: pos.id, to: d.profile ?? null });
263
+ return;
264
+ }
265
+ const upgrade = to.sizing.maxSizePctOfBankroll > from.sizing.maxSizePctOfBankroll;
266
+ if (upgrade && pos.tpFilled < 1) {
267
+ refuse('reprofile-upgrade-needs-tp1', { positionId: pos.id, to: to.id });
268
+ return;
269
+ }
270
+ ledger.add(now, 'reprofile', { positionId: pos.id, from: from.id, to: to.id, reason: d.reason });
271
+ pos.profileId = to.id;
272
+ return;
273
+ }
274
+ refuse('action-not-allowed-while-holding', { positionId: pos.id, action: d.action });
275
+ };
276
+ /** The clamp's entry as a proposal; the answer places the order (or not) whenever it lands. */
277
+ const propose = async (c, k, d, r) => {
278
+ if (!o.approvals) {
279
+ refuse('approval-unavailable', { mint: c.mint, why: 'entries need an approval and no approval channel is connected' });
280
+ await closeCandidate(c, 'refused:approval-unavailable');
281
+ return;
282
+ }
283
+ if (approvalsOpen.has(c.mint))
284
+ return;
285
+ const bank = book.bookEquity();
286
+ const R = (k.liq ?? 0) / 2;
287
+ const prof = r.profile;
288
+ const prop = {
289
+ runId: o.runId ?? null, mint: c.mint, symbol: k.symbol, strategy: { id: s.id, version: s.version, hash: s.hash }, profileId: prof.id,
290
+ usd: r.usd, firstTrancheUsd: r.firstTrancheUsd, pctOfBook: bank > 0 ? Math.round((r.usd / bank) * 1000) / 10 : 0, bookUsd: round2(bank),
291
+ price: k.price ?? null, liq: k.liq ?? null, impactPct: R > 0 ? Math.round((r.firstTrancheUsd / R) * 1e4) / 100 : 0,
292
+ slippageBps: x.slippageBps, maxImpactPct: x.maxPriceImpactPct, exitPlan: exitPlanLine(prof), reason: d.reason, sessionId: c.session?.id ?? null,
293
+ clamps: r.clamps.map((q) => q.rule), decidedAt: now,
294
+ };
295
+ const entryBlocked = () => (killed ? `halt:${haltReason ?? 'kill'}` : entriesPaused ? `paused:${entriesPaused}` : book.state.halted ? `halted:${book.state.halted}` : null);
296
+ const live = {
297
+ tick: () => latest.get(c.mint),
298
+ reclamp: (usd) => {
299
+ const t = latest.get(c.mint);
300
+ if (!t)
301
+ return { ok: false, refused: 'no-tick' };
302
+ const rc = clampEntry(book, t, { profile: prof.id, sizeUsd: usd }, killed);
303
+ return rc.ok ? { ok: true, usd: rc.usd, clamps: rc.clamps.map((q) => q.rule) } : { ok: false, refused: rc.refused };
304
+ },
305
+ blocked: entryBlocked,
306
+ };
307
+ ledger.add(now, 'approval_requested', { mint: c.mint, symbol: k.symbol, sessionId: prop.sessionId, usd: prop.usd, firstTrancheUsd: prop.firstTrancheUsd, profile: prof.id, price: prop.price, liq: prop.liq });
308
+ say(`PROPOSAL ${k.symbol}: $${prop.usd} (${prop.pctOfBook}% of book) [${prof.id}] waiting for approval`);
309
+ const settle = async (out) => {
310
+ if (!out.approved) {
311
+ ledger.add(now, 'approval_declined', { mint: c.mint, proposalId: out.proposalId ?? null, state: out.state, why: out.why });
312
+ say(`PROPOSAL ${k.symbol} ${out.state}: ${out.why}`);
313
+ await closeCandidate(c, `approval:${out.state}`);
314
+ return;
315
+ }
316
+ const blocked = entryBlocked();
317
+ if (blocked || c.closing || book.open.has(c.mint) || orders.some((q) => q.mint === c.mint)) {
318
+ refuse('approval-dropped', { mint: c.mint, proposalId: out.proposalId, why: blocked ?? (c.closing ? 'candidate closed' : 'already open or pending') });
319
+ if (!c.closing)
320
+ await closeCandidate(c, 'refused:approval-dropped');
321
+ return;
322
+ }
323
+ const usd = Math.min(out.usd, r.usd);
324
+ const scale = r.usd > 0 ? usd / r.usd : 0;
325
+ const t = latest.get(c.mint) ?? k;
326
+ ledger.add(now, 'approval_granted', { mint: c.mint, proposalId: out.proposalId, via: out.via, usd, proofId: out.proofId ? `${out.proofId.slice(0, 6)}…` : null });
327
+ say(`APPROVED ${k.symbol}: $${round2(usd)} via ${out.via}`);
328
+ const ladder = (s.sizing.ladder ?? []).slice(1).map((tr) => ({ frac: tr.frac, atPct: tr.atPct, deadline: now + tr.withinMin * 60e3, usd: usd * tr.frac }));
329
+ const first = round2(r.firstTrancheUsd * scale);
330
+ orders.push({ mint: c.mint, fillAt: now + x.latencyMs, decidedPrice: t.price ?? 0, usd: first, firstUsd: first, profile: prof.id, sessionId: c.session?.id ?? null, trigger: c.matched, ladder, kind: 'open', ctx: { proofId: out.proofId, proposalId: out.proposalId, orderHash: out.orderHash } });
331
+ };
332
+ const pending = o.approvals.request(prop, live)
333
+ .catch((e) => ({ approved: false, state: 'ERROR', why: e instanceof Error ? e.message.slice(0, 120) : String(e) }))
334
+ .then(settle)
335
+ .finally(() => { approvalsOpen.delete(c.mint); });
336
+ approvalsOpen.set(c.mint, pending);
337
+ // Lockstep (backtests, tests) waits for the answer; every other pacing lets it land when it lands.
338
+ if (pacing.mode === 'lockstep')
339
+ await pending;
340
+ };
341
+ const onReply = async (c, r) => {
342
+ if (!c.session)
343
+ return;
344
+ if (c.closing) {
345
+ // A reply that lands after its candidate expired or its session was closed is never executed.
346
+ ledger.add(now, 'late_reply_ignored', { sessionId: c.session.id, mint: c.mint, decision: r?.decision ?? null });
347
+ if (r)
348
+ say(` ← ${c.session.id} late reply ignored (candidate already closed)`);
349
+ return;
350
+ }
351
+ const review = c.awaitingReview;
352
+ c.awaitingReview = null;
353
+ if (r) {
354
+ c.turns++;
355
+ gate.turn(r.costUsd - c.costSeen);
356
+ c.costSeen = Math.max(c.costSeen, r.costUsd);
357
+ }
358
+ if (!r || !r.decision.ok) {
359
+ const died = c.session.endReason?.() ?? null;
360
+ if (!r)
361
+ ledger.add(now, 'session_timeout', { sessionId: c.session.id, mint: c.mint, ended: died });
362
+ if (died === 'account-mismatch' || died === 'not-tool-less' || died === 'account-unavailable') {
363
+ if (!gate.blocked)
364
+ ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
365
+ gate.block(died);
366
+ }
367
+ if (review)
368
+ await noAnswer(review, !r ? (died ?? 'timeout') : 'invalid-decision');
369
+ if (!r) {
370
+ // A dead process (account mismatch, a tool list that is not empty, an exit) ends the session for good; a
371
+ // slow candidate is skipped (onTimeout: skip); a slow review leaves the position to the monitor.
372
+ if (died || !c.positionId || c.oneShot)
373
+ await closeCandidate(c, died ?? (c.oneShot ? 'position-session-timeout' : 'timeout'));
374
+ return;
375
+ }
376
+ }
377
+ ledger.add(now, 'reply', { sessionId: c.session.id, mint: c.mint, ms: r.ms, costUsd: r.costUsd, text: o.verbose ? r.text : undefined, decision: r.decision });
378
+ say(` ← ${c.session.id} ${r.decision.ok ? `${r.decision.decision.action}${r.decision.decision.sizePct !== undefined ? ` ${r.decision.decision.sizePct}%` : ''}: ${r.decision.decision.reason}` : `INVALID (${r.decision.error})`}${r.ms ? ` · ${(r.ms / 1000).toFixed(1)}s` : ''} · $${r.costUsd.toFixed(4)}`);
379
+ if (r.decision.ok)
380
+ c.lastDecision = r.decision.decision.action;
381
+ if (r.costUsd >= gate.caps.budgetUsdPerSession) {
382
+ await closeCandidate(c, 'budget');
383
+ return;
384
+ }
385
+ if (r.decision.ok)
386
+ await apply(c, r.decision.decision);
387
+ // invalid decisions do nothing (the next delta asks again)
388
+ if (c.closing)
389
+ return;
390
+ if (c.oneShot) {
391
+ await closeCandidate(c, 'position-session-answered');
392
+ return;
393
+ }
394
+ const over = gate.overDay();
395
+ if (over)
396
+ await closeCandidate(c, over);
397
+ };
398
+ /** Queue a message; send now if the session is idle. Lockstep awaits the reply. */
399
+ const post = async (c, text) => {
400
+ if (!c.session || c.closing)
401
+ return;
402
+ c.pendingText.push(text);
403
+ if (c.inflight)
404
+ return;
405
+ const pump = async () => {
406
+ while (c.pendingText.length && !c.closing) {
407
+ // Once the position is gone, only the final mechanical-exit notice is still worth a turn.
408
+ if (c.positionId && !book.open.has(c.mint)) {
409
+ c.pendingText = c.pendingText.filter((t) => t.includes('EVENT mechanical_exit'));
410
+ if (!c.pendingText.length)
411
+ break;
412
+ }
413
+ // Coalesce: everything that piled up while the agent was thinking goes as ONE message.
414
+ const msg = [...c.pendingText.splice(0), ...c.notes.splice(0)].join('\n\n');
415
+ ledger.add(now, 'message', { sessionId: c.session.id, mint: c.mint, bytes: msg.length, text: o.verbose ? msg : undefined });
416
+ if (o.verbose)
417
+ say(` → ${c.session.id} ${msg.split('\n')[0]}`);
418
+ const r = await c.session.send(msg);
419
+ await onReply(c, r);
420
+ }
421
+ };
422
+ if (pacing.mode === 'lockstep') {
423
+ await pump();
424
+ return;
425
+ }
426
+ const p = pump().finally(() => { c.inflight = null; pumps.delete(p); });
427
+ c.inflight = p;
428
+ pumps.add(p);
429
+ };
430
+ const fillOrders = async () => {
431
+ for (let i = 0; i < orders.length; i++) {
432
+ const q = orders[i];
433
+ if (q.fillAt > now)
434
+ continue;
435
+ orders.splice(i--, 1);
436
+ const k = latest.get(q.mint);
437
+ // Re-check at the fill, like a swap's slippage tolerance: a pool that drained or a price that ran
438
+ // away between the decision and the fill refuses the order instead of buying into it.
439
+ const prof0 = s.exits.profiles[q.profile];
440
+ const why = (k.liq ?? 0) < prof0.fit.minLiquidityUsd ? 'fill-liquidity-gone'
441
+ : k.price && q.decidedPrice && k.price > q.decidedPrice * (1 + x.slippageBps / 1e4) ? 'fill-slippage'
442
+ // task-1538: on a shared paper account another strategy may have spent the cash since this order was decided;
443
+ // the cash is held before the (async) fill so two strategies can never spend the same dollars.
444
+ : book.account && !book.account.hold(q.usd) ? 'fill-account-cash' : null;
445
+ let f;
446
+ try {
447
+ f = why ? { refused: why } : await exec.buy(k, q.usd, q.ctx);
448
+ }
449
+ finally {
450
+ if (!why && book.account)
451
+ book.account.release(q.usd);
452
+ }
453
+ if ('refused' in f) {
454
+ const rule = f.refused.startsWith('fill-') ? f.refused : `fill-${f.refused}`;
455
+ refuse(rule, { mint: q.mint, decided: q.decidedPrice, price: k.price, liq: k.liq });
456
+ const c = cands.get(q.mint);
457
+ if (c && q.kind === 'open') {
458
+ say(`ORDER REFUSED ${k.symbol}: ${rule}`);
459
+ await closeCandidate(c, `refused:${rule}`);
460
+ }
461
+ continue;
462
+ }
463
+ if (q.kind === 'add') {
464
+ const p = book.open.get(q.mint);
465
+ if (!p || p.id !== q.positionId)
466
+ continue;
467
+ f.reason = 'ladder-add';
468
+ const bookUsd = round4(book.bookEquity()); // task-1530: the book before the fill (the trade ledger's % of book)
469
+ book.addTranche(p, f);
470
+ ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), bookUsd, ...sigTag(f) });
471
+ say(`ADD ${k.symbol}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}`);
472
+ continue;
473
+ }
474
+ const prof = s.exits.profiles[q.profile];
475
+ const bookUsd = round4(book.bookEquity()); // task-1530: the book before the fill (the trade ledger's % of book)
476
+ const p = book.openPosition(k, f, prof, q.sessionId, q.trigger, q.ladder);
477
+ 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) });
478
+ 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)}`);
479
+ const c = cands.get(q.mint);
480
+ if (c) {
481
+ c.positionId = p.id;
482
+ byPosition.set(p.id, c);
483
+ c.notes.push(`ENGINE: your entry filled: $${f.usd.toFixed(2)} at ${lvl(f.price, k)} (impact ${f.impactPct}%${f.partial ? ', PARTIAL at the impact cap' : ''}), profile ${prof.id}. The monitor runs the exits now; you hear from it only through EVENTs.`);
484
+ }
485
+ }
486
+ };
487
+ /** A review nobody answered: the profile decides (hold, or exit everything). Mechanical exits never wait. */
488
+ const noAnswer = async (w, why) => {
489
+ const p = [...book.open.values()].find((q) => q.id === w.positionId);
490
+ if (!p)
491
+ return;
492
+ const rule = s.exits.profiles[p.profileId].wake?.noAnswer ?? 'hold';
493
+ ledger.add(now, 'no_answer', { positionId: p.id, event: w.event, why, rule });
494
+ if (rule === 'exit')
495
+ await sellPosition(p, latest.get(p.mint), p.tokens, `${w.event}:no-answer`, `${w.event} unanswered (${why}); the profile says exit`);
496
+ };
497
+ /** All wakes of one tick go to the session as ONE message (one turn), after the mechanical actions ran. */
498
+ const wakeSession = async (p, ws, k) => {
499
+ if (!ws.length)
500
+ return;
501
+ let c = byPosition.get(p.id);
502
+ for (const w of ws) {
503
+ ledger.add(now, 'wake', { positionId: p.id, mint: p.mint, event: w.event, detail: w.detail, sessionId: c?.session?.id ?? null });
504
+ say(`WAKE ${p.symbol} ${w.event}: ${w.detail}`);
505
+ }
506
+ const review = ws.find((w) => REVIEW.includes(w.event));
507
+ const judgement = ws.some((w) => !INFO_ONLY.includes(w.event));
508
+ let first = null;
509
+ if ((!c?.session || c.closing) && judgement && s.agent.respawnOnWake && o.sessions && !decideAuto && !killed && p.status === 'open') {
510
+ // §5: a wake for a position whose session has ended starts a one-answer position session.
511
+ const g = gate.canOpen('position', liveCandidateSessions());
512
+ if (!g.ok)
513
+ refuse(g.reason, { stage: 'position-session', positionId: p.id, mint: p.mint });
514
+ else {
515
+ const sid = `p${++positionSessionsOpened}`;
516
+ const session = o.sessions(sid, s, { kind: 'position', mint: p.mint });
517
+ gate.opened('position');
518
+ c = newCand(k, [], session, 'position', p.id);
519
+ byPosition.set(p.id, c);
520
+ ledger.add(now, 'session_start', { sessionId: sid, sessionKind: 'position', agent: session.kind, mint: p.mint, positionId: p.id, model: sessionModel(session, s), budgetUsd: gate.caps.budgetUsdPerSession });
521
+ say(`POSITION SESSION ${sid} for ${p.symbol} (${ws.map((w) => w.event).join(', ')})`);
522
+ first = renderPositionDossier({ symbol: p.symbol, mint: p.mint, profile: p.profileId, costUsd: p.costUsd, entryPrice: p.entryPrice, tpFilled: p.tpFilled, leftPct: p.origTokens ? (p.tokens / p.origTokens) * 100 : 0, costRecovered: !!p.costRecovered, history: (hist.get(p.mint) ?? []).filter((h) => h.t >= p.openedAt), basis, tick: p.lastTick });
523
+ }
524
+ }
525
+ if (c?.session && !c.closing) {
526
+ if (review)
527
+ c.awaitingReview = review;
528
+ const events = ws.map((w) => renderEvent(w, p, k, basis)).join('\n\n');
529
+ await post(c, first ? `${first}\n\n${events}` : events);
530
+ }
531
+ else if (review)
532
+ await noAnswer(review, 'no session');
533
+ };
534
+ const bounds = () => ({ profiles: Object.keys(s.exits.profiles), defaultProfile: s.exits.default, maxSizePct: Math.min(s.sizing.maxPctOfBankroll, s.exits.profiles[s.exits.default].sizing.maxSizePctOfBankroll), sizeMode: s.sizing.mode, bankrollUsd: book.bookEquity(), cashUsd: book.state.cashUsd, openPositions: book.open.size, maxConcurrent: s.risk.maxConcurrent });
535
+ const publishFocus = () => {
536
+ if (!o.onFocus)
537
+ return;
538
+ const marks = [...[...book.open.keys()].map((mint) => ({ mint, role: 'held' })), ...[...cands.values()].filter((c) => !c.positionId).map((c) => ({ mint: c.mint, role: 'candidate' }))];
539
+ const sig = marks.map((m) => `${m.role}:${m.mint}`).sort().join(',');
540
+ if (sig === focusSig)
541
+ return;
542
+ focusSig = sig;
543
+ o.onFocus(marks);
544
+ };
545
+ const liquidationMark = (p) => { const f = simSell(p.lastTick, p.tokens, x, 'mark'); return 'refused' in f ? 0 : f.usd; };
546
+ const snapshot = (state) => {
547
+ const live = everyLive().filter((c) => c.session && !c.closing);
548
+ const day = gate.todayNow();
549
+ return {
550
+ t: now, state, haltReason: killed ? haltReason : book.state.halted ?? entriesPaused ?? gate.blocked,
551
+ strategy: { id: s.id, version: s.version, hash: s.hash },
552
+ book: { startUsd: s.bankroll.startUsd, equityUsd: round2(book.markedEquity()), cashUsd: round2(book.state.cashUsd), realizedTodayUsd: round2(book.state.dayRealizedUsd) },
553
+ ...(book.account ? { account: { id: book.account.id, pnlUsd: round2(book.ownPnlUsd()) } } : {}),
554
+ spend: { todayUsd: round4(day.modelUsd), dayCapUsd: gate.caps.modelUsdPerDay, turnsToday: day.turns, candidateSessionsToday: day.candidateSessions, positionSessionsToday: day.positionSessions, allowance: gate.allowance ? { fiveHour: gate.allowance.fiveHour, sevenDay: gate.allowance.sevenDay } : null },
555
+ candidates: [...cands.values()].filter((c) => !c.positionId).map((c) => ({ mint: c.mint, symbol: c.symbol, ageSec: Math.round((now - c.since) / 1000), matched: c.matched, session: c.session?.id ?? null })),
556
+ 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 })),
557
+ positions: [...book.open.values()].map((p) => {
558
+ const pnl = liquidationMark(p) + p.realizedUsd - p.costUsd;
559
+ 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' } : {}) };
560
+ }),
561
+ ledgerTail: ledger.events.slice(-12).map((e) => ({ seq: e.seq, t: e.t, kind: e.kind, text: tailText(e) })),
562
+ ledgerHead: ledger.head(),
563
+ events: ledger.events.length,
564
+ ...(o.real ? { real: o.real.status() } : {}),
565
+ };
566
+ };
567
+ const loop = tapeTicks ? fromArray(tapeTicks) : o.source.ticks;
568
+ for await (const k of loop) {
569
+ now = k.t;
570
+ const v = o.safety?.() ?? null;
571
+ // task-1645: receipts land in the ledger as data; the real-mode kill switch halts like the panic file.
572
+ const rs = o.real?.step() ?? null;
573
+ for (const r of rs?.receipts ?? []) {
574
+ const dv = r.divergence;
575
+ 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 } : {}) });
576
+ }
577
+ const haltWhy = v?.halt ?? (o.killSwitch?.() ? 'kill-switch' : null) ?? rs?.halt ?? null;
578
+ if (!killed && haltWhy) {
579
+ killed = true;
580
+ haltReason = haltWhy;
581
+ book.state.halted = 'kill-switch';
582
+ ledger.add(now, 'halt', { reason: haltWhy, ...(v?.detail ? { detail: v.detail } : {}), openPositions: book.open.size, droppedOrders: orders.length });
583
+ say(`${haltWhy === 'kill-switch' ? 'KILL SWITCH' : haltWhy.toUpperCase()}: no new entries, sessions closed, orders dropped, mechanical exits keep running`);
584
+ o.approvals?.cancelAll(haltWhy);
585
+ for (const c of everyLive())
586
+ await closeCandidate(c, haltWhy);
587
+ orders.length = 0;
588
+ }
589
+ if (v?.flatten && !flattening) {
590
+ flattening = true;
591
+ ledger.add(now, 'flatten', { positions: [...book.open.values()].map((p) => ({ positionId: p.id, mint: p.mint })) });
592
+ say(`FLATTEN: every paper position sells at its next tick (${book.open.size} open)`);
593
+ }
594
+ if (v?.endWhenFlat)
595
+ endWhenFlat = true;
596
+ const paused = killed ? null : v?.pauseEntries ?? null;
597
+ if (paused !== entriesPaused) {
598
+ ledger.add(now, paused ? 'entries_paused' : 'entries_resumed', paused ? { reason: paused } : { after: entriesPaused });
599
+ say(paused ? `ENTRIES PAUSED (${paused}): no new candidates; open positions keep their exits` : 'entries resumed');
600
+ entriesPaused = paused;
601
+ if (paused)
602
+ o.approvals?.cancelAll(`paused:${paused}`);
603
+ }
604
+ // Orders due by now fill at the price AS OF their fill time: the newest row before this one (rows are
605
+ // written on >= 5 % moves, so no new row means the price had not moved much). Then this row lands. On a
606
+ // single-coin tape that row can be minutes later; the fill still uses the price as of its fill time.
607
+ await fillOrders();
608
+ latest.set(k.mint, k);
609
+ const h = hist.get(k.mint) ?? [];
610
+ h.push(k);
611
+ if (h.length > 120)
612
+ h.shift();
613
+ hist.set(k.mint, h);
614
+ const haltedBefore = book.state.halted;
615
+ book.roll(now);
616
+ if (book.state.halted && book.state.halted !== haltedBefore) {
617
+ ledger.add(now, 'halt', { reason: book.state.halted });
618
+ o.approvals?.cancelAll(`halted:${book.state.halted}`);
619
+ }
620
+ // Held coin → the monitor (mechanical first, then wakes).
621
+ const pos = book.open.get(k.mint);
622
+ const chatSell = o.approvals?.takeExit?.(k.mint) ?? null;
623
+ if (chatSell && !pos)
624
+ o.approvals?.exitResult?.(chatSell.id, 'not-held');
625
+ if (pos && chatSell && !flattening) {
626
+ await sellPosition(pos, k, pos.tokens, 'chat-sell', `sell requested by ${chatSell.via}`);
627
+ o.approvals?.exitResult?.(chatSell.id, pos.status === 'closed' ? 'sold' : pos.stuck ? 'stuck' : 'partial');
628
+ if (pos.status === 'closed') {
629
+ const c = byPosition.get(pos.id);
630
+ if (c) {
631
+ if (c.inflight)
632
+ await c.inflight;
633
+ await closeCandidate(c, 'position-closed');
634
+ }
635
+ }
636
+ }
637
+ else if (pos && flattening) {
638
+ // Flatten: the whole position at this tick; an unsellable one stays stuck and is retried every tick.
639
+ await sellPosition(pos, k, pos.tokens, 'flatten', 'flatten requested');
640
+ if (pos.status === 'closed') {
641
+ const c = byPosition.get(pos.id);
642
+ if (c) {
643
+ if (c.inflight)
644
+ await c.inflight;
645
+ await closeCandidate(c, 'position-closed');
646
+ }
647
+ }
648
+ }
649
+ else if (pos) {
650
+ const prof0 = s.exits.profiles[pos.profileId];
651
+ // task-1654: a real run's (or a paper scope's) maxHoldMinutes limit tightens the profile's holding cap (never loosens it).
652
+ const holdCap = o.real?.maxHoldMinutes?.() ?? o.maxHoldMinutes?.() ?? null;
653
+ const prof = holdCap !== null && (prof0.maxHoldMinutes == null || holdCap < prof0.maxHoldMinutes) ? { ...prof0, maxHoldMinutes: holdCap } : prof0;
654
+ 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; } });
655
+ const wakes = [];
656
+ for (const a of acts) {
657
+ if (a.kind === 'sell') {
658
+ if (pos.status !== 'open')
659
+ break;
660
+ await sellPosition(pos, k, a.tokens, a.reason, a.detail);
661
+ }
662
+ else if (a.kind === 'add') {
663
+ if (killed || book.state.halted)
664
+ continue;
665
+ const room = Math.min(a.usd, book.state.cashUsd, (book.bookEquity() * s.risk.maxExposurePct) / 100 - book.exposureUsd());
666
+ if (room < s.sizing.minUsd) {
667
+ refuse('ladder-add-no-room', { positionId: pos.id });
668
+ continue;
669
+ }
670
+ ledger.add(now, 'ladder_trigger', { positionId: pos.id, detail: a.detail, usd: room });
671
+ orders.push({ mint: k.mint, fillAt: now + x.latencyMs, decidedPrice: k.price ?? 0, usd: room, firstUsd: room, profile: pos.profileId, sessionId: pos.sessionId, trigger: [], ladder: [], kind: 'add', positionId: pos.id });
672
+ }
673
+ else
674
+ wakes.push(a.wake);
675
+ }
676
+ await wakeSession(pos, wakes, k);
677
+ if (pos.status === 'closed') {
678
+ const c = byPosition.get(pos.id);
679
+ if (c) {
680
+ if (c.inflight)
681
+ await c.inflight;
682
+ await closeCandidate(c, 'position-closed');
683
+ }
684
+ }
685
+ }
686
+ else {
687
+ const c = cands.get(k.mint);
688
+ if (c) {
689
+ if (rules.screen(k).pass)
690
+ c.lastMatch = now;
691
+ if (now - c.lastMatch > s.entry.candidateTtlSec * 1000 && !orders.some((q) => q.mint === c.mint) && !approvalsOpen.has(c.mint))
692
+ await closeCandidate(c, 'candidate-ttl');
693
+ else if (c.session && !c.positionId) {
694
+ const cur = view(k);
695
+ const due = (now - c.lastSentT) / 1000 >= s.agent.deltaEverySec || urgent(c.lastSent, cur);
696
+ const d = due ? buildDelta(c.lastSent, cur, c.seq + 1, c.since) : null;
697
+ if (d) {
698
+ c.seq++;
699
+ c.lastSent = cur;
700
+ c.lastSentT = now;
701
+ await post(c, renderDelta(d));
702
+ }
703
+ }
704
+ }
705
+ else if (!killed && !entriesPaused && !book.state.halted && (skippedUntil.get(k.mint) ?? 0) <= now) {
706
+ const sr = rules.screen(k);
707
+ if (sr.pass) {
708
+ // Cheap pre-check so a full book does not spawn sessions it could never act on.
709
+ const pre = clampEntry(book, k, {}, killed);
710
+ const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k) : null;
711
+ if (!pre.ok) {
712
+ if (!['already-open'].includes(pre.refused))
713
+ count(`precheck:${pre.refused}`);
714
+ }
715
+ else if (v && !v.ok) {
716
+ refuse(v.rule, { mint: k.mint, symbol: k.symbol, stage: 'vet', detail: v.detail });
717
+ skippedUntil.set(k.mint, Infinity);
718
+ say(`SKIP ${k.symbol} (${k.mint.slice(0, 4)}…): ${v.rule} (${v.detail})`);
719
+ }
720
+ else if (decideAuto) {
721
+ const cand = newCand(k, sr.matched, null, 'candidate', null);
722
+ cands.set(k.mint, cand);
723
+ ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
724
+ await apply(cand, { action: 'enter', reason: 'auto' });
725
+ if (!orders.some((q) => q.mint === k.mint) && !approvalsOpen.has(k.mint))
726
+ cands.delete(k.mint);
727
+ }
728
+ else {
729
+ const g = gate.canOpen('candidate', liveCandidateSessions());
730
+ if (!g.ok)
731
+ count(g.reason);
732
+ else {
733
+ const sid = `s${++sessionsOpened}`;
734
+ const session = o.sessions(sid, s, { kind: 'candidate', mint: k.mint });
735
+ gate.opened('candidate');
736
+ const cand = newCand(k, sr.matched, session, 'candidate', null);
737
+ cands.set(k.mint, cand);
738
+ ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
739
+ ledger.add(now, 'session_start', { sessionId: sid, sessionKind: 'candidate', agent: session.kind, mint: k.mint, model: sessionModel(session, s), budgetUsd: gate.caps.budgetUsdPerSession });
740
+ say(`CANDIDATE ${k.symbol} (${k.mint.slice(0, 4)}…) matched ${sr.matched.join(', ')} → session ${sid} (${session.kind})`);
741
+ const text = await dossierFn(k, h.slice(0, -1), sr.matched, bounds());
742
+ await post(cand, text);
743
+ }
744
+ }
745
+ }
746
+ }
747
+ }
748
+ // Session lifetime: a position that closed by ANY path (monitor, agent exit, kill) ends its session;
749
+ // any session ends at sessionMaxMin (a held position keeps its mechanical exits; respawnOnWake can reopen one).
750
+ for (const c of everyLive()) {
751
+ if (c.positionId && !book.open.has(c.mint)) {
752
+ if (c.inflight)
753
+ await c.inflight;
754
+ await closeCandidate(c, 'position-closed');
755
+ continue;
756
+ }
757
+ if (c.session && now - c.since > s.agent.sessionMaxMin * 60e3)
758
+ await closeCandidate(c, 'session-max-minutes');
759
+ }
760
+ publishFocus();
761
+ const eq = book.markedEquity();
762
+ if (!curve.length || curve[curve.length - 1].equity !== eq)
763
+ curve.push({ t: now, equity: eq });
764
+ maxDd = Math.max(maxDd, ((book.state.peakEquityUsd - eq) / book.state.peakEquityUsd) * 100);
765
+ if (o.onState)
766
+ o.onState(snapshot(killed || book.state.halted ? 'halted' : entriesPaused || gate.blocked || gate.paused() ? 'paused' : 'running'));
767
+ // A stop or flatten ends the run once nothing is held and nothing is pending (the panic file alone keeps it up).
768
+ if (killed && endWhenFlat && !book.open.size && !orders.length) {
769
+ ledger.add(now, 'stopped_flat', { reason: haltReason });
770
+ say('nothing held: the runner stops');
771
+ break;
772
+ }
773
+ // Paced replays fast-forward while no session is live (nothing is waiting on the market).
774
+ if (pacing.mode === 'paced' && everyLive().some((c) => c.session && !c.closing))
775
+ await sleep(pacing.msPerTick);
776
+ }
777
+ // End of the tape / source: open proposals are cancelled, replies in flight land, sessions close, what is still open is marked.
778
+ if (approvalsOpen.size) {
779
+ o.approvals?.cancelAll(o.tape ? 'end-of-tape' : 'runner-stopped');
780
+ await Promise.allSettled([...approvalsOpen.values()]);
781
+ }
782
+ for (const c of everyLive()) {
783
+ if (c.inflight)
784
+ await c.inflight;
785
+ await closeCandidate(c, o.tape ? 'end-of-tape' : 'runner-stopped');
786
+ }
787
+ while (pumps.size)
788
+ await Promise.allSettled([...pumps]);
789
+ const openAtEnd = [...book.open.values()].map((p) => ({ mint: p.mint, symbol: p.symbol, markUsd: round2(liquidationMark(p)), costUsd: p.costUsd, stuck: p.stuck }));
790
+ if (o.real) {
791
+ // The dead-man's graceful half: the executor hears the run is over (its outstanding nonces may be cancelled).
792
+ const why = killed ? `halt:${haltReason ?? 'kill'}` : o.tape ? 'end-of-tape' : 'runner-stopped';
793
+ // r2: what is still open is carried (held.json): the next real run of this strategy adopts it and keeps its exits.
794
+ o.real.carry([...book.open.values()]);
795
+ o.real.sendHalted(why);
796
+ ledger.add(now, 'real_halted', { tag: 'real-signal', reason: why, openAtEnd: openAtEnd.length });
797
+ }
798
+ 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() });
799
+ publishFocus();
800
+ if (o.onState)
801
+ o.onState(snapshot('stopped'));
802
+ return { trades: book.closed, openAtEnd, ledger, book, candidates: ledger.events.filter((e) => e.kind === 'candidate').length, sessions: sessionsOpened + positionSessionsOpened, sessionCostUsd: sessionCost, maxDrawdownPct: Math.round(maxDd * 100) / 100, equityCurve: curve, refusals, clamps: clampCount };
803
+ }
804
+ const round2 = (v) => Math.round(v * 100) / 100;
805
+ /** The exit plan of a profile in one line, for a proposal (engine state only). */
806
+ export function exitPlanLine(p) {
807
+ const parts = [];
808
+ if (p.costRecovery)
809
+ parts.push(`cost back at +${p.costRecovery.atPct}%`);
810
+ if (p.bagTrail)
811
+ parts.push(`then a ${p.bagTrail.trailPct}% bag trail`);
812
+ if (p.tpLadder.length)
813
+ parts.push(`take profit ${p.tpLadder.map((t) => `+${t.atPct}%`).join(', ')}`);
814
+ if (p.trailingStop)
815
+ parts.push(`${p.trailingStop.trailPct}% trail from +${p.trailingStop.armAtPct}%`);
816
+ if (p.maxHoldMinutes) {
817
+ const m = p.maxHoldMinutes;
818
+ parts.push(`out after ${m >= 2880 ? `${Math.round(m / 1440)} days` : m >= 120 ? `${Math.round(m / 60)} h` : `${m} min`}`);
819
+ }
820
+ parts.push(p.hardStop.enabled && p.hardStop.atPct !== null ? `stop ${p.hardStop.atPct}%` : 'no hard stop');
821
+ return `${p.id}: ${parts.join(', ')}`;
822
+ }
823
+ const round4 = (v) => Math.round(v * 1e4) / 1e4;
824
+ /** task-1645: a fill that went out as a trade-fire signal: the ledger row is tagged and names the signal. */
825
+ 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 } : {}) } : {}; };
826
+ 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 });
827
+ /** Infinity (no runner cap) is written as null so the ledger stays JSON. */
828
+ /** The model a session actually runs (a run's --model override wins over the strategy's), for the ledger. */
829
+ // task-1543: any model-backed session (claude, ollama) records the model it runs; only the stub has none.
830
+ const sessionModel = (x, s) => (x.kind === 'stub' ? 'stub' : x.model ?? s.agent.model);
831
+ const capsView = (g) => Object.fromEntries(Object.entries(g.caps).map(([k, v]) => [k, Number.isFinite(v) ? v : null]));
832
+ /** One short line per ledger event for the runner view (no message text: that stays in the ledger). */
833
+ function tailText(e) {
834
+ const pick = (...ks) => ks.map((k) => (e[k] === undefined || e[k] === null ? null : `${k} ${typeof e[k] === 'object' ? JSON.stringify(e[k]).slice(0, 60) : String(e[k]).slice(0, 60)}`)).filter(Boolean).join(' · ');
835
+ switch (e.kind) {
836
+ case 'candidate': return pick('symbol', 'matched');
837
+ case 'session_start':
838
+ case 'session_end': return pick('sessionId', 'sessionKind', 'reason', 'turns', 'costUsd');
839
+ case 'reply': {
840
+ const d = e.decision;
841
+ return `${String(e.sessionId)} ${d?.ok ? `${d.decision.action}: ${d.decision.reason.slice(0, 80)}` : `invalid: ${d?.error ?? '?'}`}`;
842
+ }
843
+ case 'fill': return pick('side', 'usd', 'price', 'reason');
844
+ case 'refused': return pick('rule', 'stage');
845
+ case 'wake': return pick('event', 'detail');
846
+ default: return pick('reason', 'mint', 'rule');
847
+ }
848
+ }
849
+ // ───────────────────────────── host: lock, rendezvous, heartbeat ─────────────────────────────
850
+ //
851
+ // One runner per machine: $DEXTERM_HOME/autotrade/run.lock holds {pid, bootAt, runId, startedAt}. A lock whose pid is
852
+ // gone, or that was written before this boot, is stale and reclaimed; a live one refuses the second runner.
853
+ // The TUI's runner view and the monitor's dead-man read the rendezvous, rewritten (atomic) every 10 s and on change.
854
+ export const RUNNER_RENDEZVOUS_SCHEMA = 'dexterm-autotrade-runner/1';
855
+ export const HEARTBEAT_MS = 10_000;
856
+ export const runLockPath = (home) => join(home, 'autotrade', 'run.lock');
857
+ /** The rendezvous (`dexterm-autotrade-runner/1`); runner.json next to it is the runner CONFIG (§7.3). */
858
+ export const runnerRendezvousPath = (home) => join(home, 'autotrade', 'runner-status.json');
859
+ const bootAt = () => Math.round((Date.now() - uptime() * 1000) / 60_000) * 60_000;
860
+ const alive = (pid) => { try {
861
+ process.kill(pid, 0);
862
+ return true;
863
+ }
864
+ catch (e) {
865
+ return e.code === 'EPERM';
866
+ } };
867
+ export function acquireRunLock(home, runId, now = Date.now()) {
868
+ const path = runLockPath(home);
869
+ mkdirSync(join(home, 'autotrade'), { recursive: true, mode: 0o700 });
870
+ const lock = { pid: process.pid, bootAt: bootAt(), runId, startedAt: now };
871
+ let reclaimed = null;
872
+ for (let attempt = 0; attempt < 2; attempt++) {
873
+ try {
874
+ const fd = openSync(path, 'wx', 0o600);
875
+ writeSync(fd, `${JSON.stringify(lock)}\n`);
876
+ closeSync(fd);
877
+ return { ok: true, lock, reclaimed };
878
+ }
879
+ catch (e) {
880
+ if (e.code !== 'EEXIST')
881
+ throw e;
882
+ }
883
+ let held = null;
884
+ try {
885
+ held = JSON.parse(readFileSync(path, 'utf8'));
886
+ }
887
+ catch {
888
+ held = null;
889
+ }
890
+ const stale = !held || typeof held.pid !== 'number' || !alive(held.pid) || Math.abs((held.bootAt ?? 0) - lock.bootAt) > 5 * 60_000;
891
+ if (!stale)
892
+ return { ok: false, holder: held };
893
+ reclaimed = held;
894
+ rmSync(path, { force: true });
895
+ }
896
+ throw new Error(`could not take ${path}`);
897
+ }
898
+ export function releaseRunLock(home, runId) {
899
+ const path = runLockPath(home);
900
+ try {
901
+ const held = JSON.parse(readFileSync(path, 'utf8'));
902
+ if (held.runId === runId && held.pid === process.pid)
903
+ rmSync(path, { force: true });
904
+ }
905
+ catch { /* gone */ }
906
+ }
907
+ export function writeRendezvous(home, r) {
908
+ mkdirSync(join(home, 'autotrade'), { recursive: true, mode: 0o700 });
909
+ writeJsonAtomic(runnerRendezvousPath(home), r);
910
+ }
911
+ /** Age of the runner's heartbeat in ms (Infinity when there is none): the monitor refuses entries on a stale one. */
912
+ export function runnerHeartbeatAge(home, now = Date.now()) {
913
+ const p = runnerRendezvousPath(home);
914
+ if (!existsSync(p))
915
+ return Infinity;
916
+ try {
917
+ const d = JSON.parse(readFileSync(p, 'utf8'));
918
+ return d.state === 'stopped' || typeof d.heartbeatAt !== 'number' ? Infinity : now - d.heartbeatAt;
919
+ }
920
+ catch {
921
+ return Infinity;
922
+ }
923
+ }