dexterm-cli 0.1.6 → 0.1.7

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