dexterm-cli 0.1.5 → 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 (379) hide show
  1. package/dist/packages/agents/src/index.js +2 -0
  2. package/dist/packages/agents/src/schema.js +22 -1
  3. package/dist/packages/agents/src/spectator.js +54 -0
  4. package/dist/packages/agents/src/story.js +532 -0
  5. package/dist/packages/agents/src/tiers.js +28 -2
  6. package/dist/packages/agents/src/tools.js +31 -2
  7. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -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/src/account.js +74 -0
  20. package/dist/packages/autotrade/src/backtest.js +357 -0
  21. package/dist/packages/autotrade/src/basis-text.js +32 -0
  22. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  23. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  24. package/dist/packages/autotrade/src/chat/config.js +93 -0
  25. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  26. package/dist/packages/autotrade/src/chat/control.js +103 -0
  27. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  28. package/dist/packages/autotrade/src/chat/format.js +71 -0
  29. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  30. package/dist/packages/autotrade/src/chat/index.js +18 -0
  31. package/dist/packages/autotrade/src/chat/notify.js +292 -0
  32. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  33. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  34. package/dist/packages/autotrade/src/chat/secrets.js +99 -0
  35. package/dist/packages/autotrade/src/chat/service.js +248 -0
  36. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  37. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  38. package/dist/packages/autotrade/src/engine.js +387 -0
  39. package/dist/packages/autotrade/src/execution-port.js +97 -0
  40. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  41. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  42. package/dist/packages/autotrade/src/fleet.js +639 -0
  43. package/dist/packages/autotrade/src/index.js +53 -0
  44. package/dist/packages/autotrade/src/ledger.js +108 -0
  45. package/dist/packages/autotrade/src/live-source.js +182 -0
  46. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  47. package/dist/packages/autotrade/src/monitor.js +190 -0
  48. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  49. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  50. package/dist/packages/autotrade/src/replay.js +198 -0
  51. package/dist/packages/autotrade/src/runner-config.js +99 -0
  52. package/dist/packages/autotrade/src/runner.js +872 -0
  53. package/dist/packages/autotrade/src/safety.js +243 -0
  54. package/dist/packages/autotrade/src/session.js +269 -0
  55. package/dist/packages/autotrade/src/sessions.js +503 -0
  56. package/dist/packages/autotrade/src/store.js +250 -0
  57. package/dist/packages/autotrade/src/strategy.js +256 -0
  58. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  59. package/dist/packages/autotrade/src/yolo.js +140 -0
  60. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  61. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
  62. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
  63. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  64. package/dist/packages/cli/src/agent-cmd.js +6 -4
  65. package/dist/packages/cli/src/agent-spectator.js +17 -1
  66. package/dist/packages/cli/src/ai-cmd.js +412 -0
  67. package/dist/packages/cli/src/analyst/backend.js +105 -15
  68. package/dist/packages/cli/src/analyst/codex.js +14 -9
  69. package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
  70. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  71. package/dist/packages/cli/src/analyst/session.js +17 -3
  72. package/dist/packages/cli/src/app-help-gen.js +51 -15
  73. package/dist/packages/cli/src/autotrade-cmd/audit.js +57 -0
  74. package/dist/packages/cli/src/autotrade-cmd/backtest.js +199 -0
  75. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  76. package/dist/packages/cli/src/autotrade-cmd/fleet.js +397 -0
  77. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  78. package/dist/packages/cli/src/autotrade-cmd/run.js +413 -0
  79. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  80. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  81. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  82. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  83. package/dist/packages/cli/src/autotrade-cmd.js +168 -0
  84. package/dist/packages/cli/src/backtest-cmd.js +13 -2
  85. package/dist/packages/cli/src/display.js +208 -23
  86. package/dist/packages/cli/src/doctor.js +62 -3
  87. package/dist/packages/cli/src/export.js +10 -2
  88. package/dist/packages/cli/src/index.js +1 -1
  89. package/dist/packages/cli/src/ledger-cmd.js +288 -0
  90. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  91. package/dist/packages/cli/src/live/dev-history.js +31 -15
  92. package/dist/packages/cli/src/live/index.js +28 -90
  93. package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
  94. package/dist/packages/cli/src/live/screens/token-live.js +48 -15
  95. package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
  96. package/dist/packages/cli/src/live/trade.js +30 -453
  97. package/dist/packages/cli/src/live/watchlist.js +1 -1
  98. package/dist/packages/cli/src/main.js +290 -199
  99. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  100. package/dist/packages/cli/src/mcp-follow.js +103 -6
  101. package/dist/packages/cli/src/plan-cmd.js +15 -5
  102. package/dist/packages/cli/src/pnl-app.js +31 -21
  103. package/dist/packages/cli/src/pnl-card.js +158 -21
  104. package/dist/packages/cli/src/popup.js +38 -6
  105. package/dist/packages/cli/src/record-cmd.js +141 -20
  106. package/dist/packages/cli/src/scan/ui.js +7 -1
  107. package/dist/packages/cli/src/screens/agent-hud.js +773 -0
  108. package/dist/packages/cli/src/screens/analyst-session.js +11 -4
  109. package/dist/packages/cli/src/screens/analyst.js +3 -2
  110. package/dist/packages/cli/src/screens/autotrade-keys.js +169 -0
  111. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  112. package/dist/packages/cli/src/screens/autotrade-screen.js +725 -0
  113. package/dist/packages/cli/src/screens/autotrade-ui.js +640 -0
  114. package/dist/packages/cli/src/screens/autotrade-worker.js +89 -0
  115. package/dist/packages/cli/src/screens/brain.js +634 -0
  116. package/dist/packages/cli/src/screens/newpairs.js +1 -1
  117. package/dist/packages/cli/src/screens/overlay.js +5 -2
  118. package/dist/packages/cli/src/screens/plans.js +5 -17
  119. package/dist/packages/cli/src/screens/pnl-chip.js +144 -0
  120. package/dist/packages/cli/src/screens/positions.js +6 -28
  121. package/dist/packages/cli/src/screens/screener.js +1 -0
  122. package/dist/packages/cli/src/screens/ticket.js +38 -205
  123. package/dist/packages/cli/src/screens/token.js +18 -9
  124. package/dist/packages/cli/src/screens/trending.js +1 -1
  125. package/dist/packages/cli/src/state.js +88 -18
  126. package/dist/packages/cli/src/telegram-cmd.js +451 -0
  127. package/dist/packages/cli/src/trade.js +3 -128
  128. package/dist/packages/cli/src/version.js +1 -1
  129. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  130. package/dist/packages/cli/src/wallet-command.js +8 -11
  131. package/dist/packages/cli/src/widgets.js +0 -12
  132. package/dist/packages/custom/src/config.js +20 -2
  133. package/dist/packages/custom/src/schema.js +41 -10
  134. package/dist/packages/custom/src/workspace.js +3 -3
  135. package/dist/packages/data/src/basis.js +191 -0
  136. package/dist/packages/data/src/detail.js +53 -2
  137. package/dist/packages/data/src/index.js +2 -14
  138. package/dist/packages/data/src/larp/score.js +220 -0
  139. package/dist/packages/data/src/larp/x-account.js +316 -0
  140. package/dist/packages/data/src/store.js +14 -5
  141. package/dist/packages/data/src/trade/agent/paper-desk.js +300 -23
  142. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  143. package/dist/packages/data/src/trade/bridge.js +4 -102
  144. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  145. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  146. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  147. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  148. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  149. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  150. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  151. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  152. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  153. package/dist/packages/data/src/trade/plans/engine.js +7 -0
  154. package/dist/packages/data/src/trade/plans/index.js +0 -1
  155. package/dist/packages/data/src/trade/plans/mandate.js +28 -15
  156. package/dist/packages/data/src/trade/plans/service.js +4 -28
  157. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  158. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  159. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  160. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  161. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  162. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  163. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  164. package/dist/packages/mcp/assets/kb/app-help/dev-history.md +1 -1
  165. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
  166. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  167. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  168. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  169. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  170. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  171. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
  172. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  173. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +217 -0
  174. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  175. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  176. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  177. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  178. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +6 -0
  179. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  180. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  181. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
  182. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  183. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  184. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +7 -3
  186. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  188. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  189. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +49 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +37 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +35 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +43 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  219. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  220. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  223. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  225. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +9 -7
  226. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  227. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  228. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  229. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  230. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  231. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  232. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  234. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  235. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
  236. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  237. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
  240. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
  241. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
  242. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  243. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  244. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +33 -0
  246. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  247. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  248. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  249. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  250. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  251. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  254. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  255. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  256. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  257. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  264. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +4 -13
  266. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  267. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +40 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  269. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  271. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  273. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  274. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  275. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
  277. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +3 -1
  278. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +10 -7
  281. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  282. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
  284. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
  288. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
  289. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  290. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  291. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  292. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  293. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  295. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  297. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  298. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  299. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  301. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  302. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  304. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  305. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  306. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  307. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  308. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  309. package/dist/packages/mcp/assets/kb/entries/autotrade.md +38 -0
  310. package/dist/packages/mcp/src/app-help.js +1 -1
  311. package/dist/packages/mcp/src/autotrade-tools.js +944 -0
  312. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  313. package/dist/packages/mcp/src/engine/config.js +159 -0
  314. package/dist/packages/mcp/src/engine/index.js +10 -0
  315. package/dist/packages/mcp/src/engine/loop.js +101 -0
  316. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  317. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  318. package/dist/packages/mcp/src/engine/probe.js +110 -0
  319. package/dist/packages/mcp/src/engine/reason.js +35 -0
  320. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  321. package/dist/packages/mcp/src/index.js +7 -1
  322. package/dist/packages/mcp/src/ledger-tools.js +346 -0
  323. package/dist/packages/mcp/src/paper-agent.js +5 -2
  324. package/dist/packages/mcp/src/server.js +285 -36
  325. package/dist/packages/mcp/src/spectator-sink.js +85 -0
  326. package/dist/packages/packs/src/backtest.js +3 -1
  327. package/dist/packages/packs/src/index.js +1 -0
  328. package/dist/packages/packs/src/larp-score-backtest.js +119 -0
  329. package/dist/packages/popup/src/pages/chart-lib.js +169 -35
  330. package/dist/packages/popup/src/pages/chart.css +18 -0
  331. package/dist/packages/popup/src/pages/chart.html +6 -1
  332. package/dist/packages/popup/src/pages/chart.js +136 -10
  333. package/dist/packages/popup/src/pages/spectator.css +45 -0
  334. package/dist/packages/popup/src/pages/spectator.html +1 -0
  335. package/dist/packages/popup/src/pages/spectator.js +69 -0
  336. package/dist/packages/popup/src/protocol.js +4 -0
  337. package/dist/packages/popup/src/server.js +9 -1
  338. package/dist/packages/recorder/src/dev-history.js +7 -3
  339. package/dist/packages/recorder/src/index.js +2 -0
  340. package/dist/packages/recorder/src/recorder.js +63 -6
  341. package/dist/packages/recorder/src/retention.js +100 -0
  342. package/dist/packages/recorder/src/schema.js +34 -1
  343. package/dist/packages/recorder/src/ticks.js +237 -0
  344. package/dist/packages/tui/src/canvas.js +7 -0
  345. package/dist/packages/tui/src/fmt.js +53 -0
  346. package/dist/packages/tui/src/index.js +1 -0
  347. package/dist/packages/tui/src/keymap.js +25 -1
  348. package/dist/packages/tui/src/markers.js +219 -0
  349. package/dist/packages/tui/src/widgets.js +158 -6
  350. package/package.json +2 -1
  351. package/dist/packages/data/src/trade/caps.js +0 -323
  352. package/dist/packages/data/src/trade/executor.js +0 -933
  353. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  354. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  355. package/dist/packages/data/src/trade/signer/client.js +0 -197
  356. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  357. package/dist/packages/data/src/trade/signer/host.js +0 -97
  358. package/dist/packages/data/src/trade/signer/main.js +0 -121
  359. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  360. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  361. package/dist/packages/data/src/trade/signer/service.js +0 -400
  362. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  363. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  364. package/dist/packages/data/src/trade/swap-build.js +0 -108
  365. package/dist/packages/data/src/trade/token-delta.js +0 -201
  366. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  367. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  368. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  369. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  370. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  371. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  372. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  373. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  374. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  375. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  376. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  377. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  378. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  379. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -0,0 +1,715 @@
1
+ // dexterm autotrade — the position monitor process (task-1506 AT-4, AUTOTRADE-SPEC §5, §6.3, §7.4). PAPER ONLY.
2
+ //
3
+ // Its own loop next to the runner, with no LLM, so a stuck or slow session can never delay an exit. Per tick of a
4
+ // held coin it runs `monitorTick` (the §5 order: rug / liquidity pull → thesis breaks → max hold → hard stop →
5
+ // cost recovery → TP ladder → bag trail → trailing stop → time stop → ladder adds; a full exit ends the tick) and
6
+ // executes each sell through the paper desk at once, journaled there as agent.paper.exit.<rule>. Only then do the
7
+ // tick's wakes go to the position's session, all of them in ONE message.
8
+ //
9
+ // What a session can do with a wake: hold, exit, trim, tighten, or re-profile along an allowed edge. It can never
10
+ // add size and never block or undo a mechanical exit: replies are applied when they land (serialized with the
11
+ // ticks), a reply for a closed position or after its deadline is logged and ignored, and nobody answering a time
12
+ // review applies the profile's `wake.noAnswer` (the other wakes hold; the mechanical rules keep running).
13
+ //
14
+ // Positions come from the paper journal: every autotrade entry registers a desk exit plan (desk-executor.ts), and
15
+ // the monitor adopts each live plan it finds there. A restart rebuilds the same way (paper journal + run ledgers +
16
+ // the last status checkpoint for peaks and windows), and never re-sends an exit that was journaled but not filled:
17
+ // the next tick decides afresh from the fills.
18
+ //
19
+ // Dead-man: the monitor writes $DEXTERM_HOME/autotrade/monitor-status.json every 10 s; the desk executor refuses
20
+ // entries when it is stale (30 s), and the monitor refuses ladder adds when the runner's heartbeat is stale. The
21
+ // monitor itself keeps exiting whether the runner is up or not, and no guard pauses it.
22
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { monitorTick, pnlPct } from "./monitor.js";
25
+ import { Ledger } from "./ledger.js";
26
+ import { runnerHeartbeatAge } from "./runner.js";
27
+ import { parseDecision, renderEvent } from "./session.js";
28
+ import { withMcap } from '../../data/src/basis.js';
29
+ import { DEFAULT_PRICE_BASIS, tickHints } from "./basis-text.js";
30
+ export const MONITOR_STATUS_SCHEMA = 'dexterm-autotrade-monitor/1';
31
+ export const MONITOR_HEARTBEAT_MS = 10_000;
32
+ /** Three missed beats: the desk executor refuses entries past this. */
33
+ export const MONITOR_STALE_MS = 30_000;
34
+ /** The runner's rendezvous (task-1505 writes it every 10 s): ladder adds need it fresher than this. */
35
+ export const RUNNER_STALE_MS = 30_000;
36
+ export const monitorStatusPath = (home) => join(home, 'autotrade', 'monitor-status.json');
37
+ export const monitorLockPath = (home) => join(home, 'autotrade', 'monitor.lock');
38
+ export const monitorLedgerDir = (home) => join(home, 'autotrade', 'monitor');
39
+ function heartbeatAge(path, now) {
40
+ try {
41
+ const d = JSON.parse(readFileSync(path, 'utf8'));
42
+ return d.state === 'stopped' || typeof d.heartbeatAt !== 'number' ? Infinity : Math.max(0, now - d.heartbeatAt);
43
+ }
44
+ catch {
45
+ return Infinity;
46
+ }
47
+ }
48
+ /** Age of the monitor's heartbeat in ms (Infinity when there is none or it stopped cleanly). */
49
+ export const monitorHeartbeatAge = (home, now = Date.now()) => heartbeatAge(monitorStatusPath(home), now);
50
+ export const monitorAlive = (home, now = Date.now()) => monitorHeartbeatAge(home, now) <= MONITOR_STALE_MS;
51
+ /** True while the runner's heartbeat (its `dexterm-autotrade-runner/1` rendezvous, task-1505) is fresh. */
52
+ export const runnerHeartbeatFresh = (home, now = Date.now()) => runnerHeartbeatAge(home, now) <= RUNNER_STALE_MS;
53
+ /** A sink over in-process sessions (a runner's candidate session, or a respawned position session). */
54
+ export function sessionWakeSink(sessionFor) {
55
+ return {
56
+ deliver(batch, p) {
57
+ const s = sessionFor(p, batch);
58
+ if (!s)
59
+ return null;
60
+ return s.send(batch.text).then((r) => (r && r.decision.ok ? r.decision.decision : null), () => null);
61
+ },
62
+ };
63
+ }
64
+ // ───────────────────────────── cross-process wake mailbox ─────────────────────────────
65
+ //
66
+ // For a runner in another process: the monitor appends each batch to autotrade/wakes/outbox.jsonl; the runner
67
+ // (which holds the position's session) appends the session's raw reply to answers.jsonl. The monitor parses it with
68
+ // the closed DECISION schema, so the file can never carry more than a decision.
69
+ export const wakeOutboxPath = (home) => join(home, 'autotrade', 'wakes', 'outbox.jsonl');
70
+ export const wakeAnswersPath = (home) => join(home, 'autotrade', 'wakes', 'answers.jsonl');
71
+ function appendLine(path, doc) {
72
+ mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 });
73
+ appendFileSync(path, `${JSON.stringify(doc)}\n`, { mode: 0o600 });
74
+ }
75
+ function readLines(path) {
76
+ if (!existsSync(path))
77
+ return [];
78
+ const out = [];
79
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
80
+ if (!line.trim())
81
+ continue;
82
+ try {
83
+ out.push(JSON.parse(line));
84
+ }
85
+ catch { /* a torn last line */ }
86
+ }
87
+ return out;
88
+ }
89
+ /** Runner side: the wake batches waiting for an answer (newest state per id). */
90
+ export function readWakeOutbox(home, now = Date.now()) {
91
+ const answered = new Set(readLines(wakeAnswersPath(home)).map((a) => a.id));
92
+ return readLines(wakeOutboxPath(home)).filter((b) => !answered.has(b.id) && b.deadline >= now);
93
+ }
94
+ /** Runner side: hand the session's raw reply text back (parsed by the monitor). */
95
+ export function answerWake(home, id, replyText) {
96
+ appendLine(wakeAnswersPath(home), { id, text: String(replyText).slice(0, 4000) });
97
+ }
98
+ export class MailboxWakeSink {
99
+ home;
100
+ hasSession;
101
+ waiting = new Map();
102
+ constructor(home, hasSession) { this.home = home; this.hasSession = hasSession; }
103
+ deliver(batch, p) {
104
+ if (!this.hasSession(p))
105
+ return null;
106
+ appendLine(wakeOutboxPath(this.home), batch);
107
+ return new Promise((res) => this.waiting.set(batch.id, res));
108
+ }
109
+ /** Pick up answers (call every cycle). */
110
+ poll() {
111
+ if (!this.waiting.size)
112
+ return;
113
+ for (const a of readLines(wakeAnswersPath(this.home))) {
114
+ const res = this.waiting.get(a.id);
115
+ if (!res)
116
+ continue;
117
+ this.waiting.delete(a.id);
118
+ const d = parseDecision(String(a.text ?? ''));
119
+ res(d.ok ? d.decision : null);
120
+ }
121
+ }
122
+ /** Settle a batch the monitor gave up on (its deadline passed). */
123
+ drop(id) { const res = this.waiting.get(id); if (res) {
124
+ this.waiting.delete(id);
125
+ res(null);
126
+ } }
127
+ }
128
+ const REVIEW = ['time_review', 'time_tighten'];
129
+ const r6 = (x) => Number(x.toPrecision(6));
130
+ /** A tick with only what a recovered position knows (prices fill in on the next real tick). */
131
+ export function bareTick(mint, symbol, t, price, liq = null) {
132
+ return {
133
+ mint, t, kind: 'live', symbol, launchpad: 'unknown', createdAt: null, price, mcap: null, mcapResolved: null, mcapFlagged: false,
134
+ liq, vol5m: null, vol1h: null, vol24: null, buys5m: null, sells5m: null, buys24: null, sells24: null, chg5m: null, chg1h: null, chg6h: null, chg24h: null,
135
+ holders: null, top10: null, dev: null, mintAuth: null, freezeAuth: null, org: null,
136
+ };
137
+ }
138
+ export class PositionMonitor {
139
+ strategy;
140
+ ledger;
141
+ /** Open positions by mint (the paper wallet holds one position per mint). */
142
+ positions = new Map();
143
+ closed = [];
144
+ exec;
145
+ wakes;
146
+ runnerAlive;
147
+ decideMs;
148
+ now;
149
+ say;
150
+ basis;
151
+ pending = new Map();
152
+ chain = Promise.resolve();
153
+ batchSeq = 0;
154
+ constructor(o) {
155
+ if (o.strategy.mode !== 'paper')
156
+ throw new Error(`strategy ${o.strategy.id} is mode "${o.strategy.mode}": the autotrade monitor runs paper only`);
157
+ this.strategy = o.strategy;
158
+ this.exec = o.executor;
159
+ this.wakes = o.wakes ?? null;
160
+ this.ledger = o.ledger ?? new Ledger();
161
+ this.runnerAlive = o.runnerAlive ?? (() => true);
162
+ this.decideMs = o.decideWithinMs ?? o.strategy.agent.decideWithinSec * 1000;
163
+ this.now = o.now ?? Date.now;
164
+ this.say = o.onLine ?? (() => { });
165
+ this.basis = o.basis ?? DEFAULT_PRICE_BASIS;
166
+ }
167
+ /** Serialize every mutation (ticks and replies), so a reply never interleaves with a tick's sells. */
168
+ serial(fn) {
169
+ const run = this.chain.then(fn, fn);
170
+ this.chain = run.catch(() => { });
171
+ return run;
172
+ }
173
+ profileOf(p) {
174
+ return this.strategy.exits.profiles[p.profileId] ?? this.strategy.exits.profiles[this.strategy.exits.default];
175
+ }
176
+ /** Take over a position (adopted from the journal or handed over in-process). */
177
+ adopt(p, why) {
178
+ if (this.positions.has(p.mint))
179
+ return;
180
+ this.positions.set(p.mint, p);
181
+ this.ledger.add(this.now(), 'adopt', { positionId: p.id, mint: p.mint, symbol: p.symbol, profile: p.profileId, why, tokens: r6(p.tokens), costUsd: r6(p.costUsd), realizedUsd: r6(p.realizedUsd), tpFilled: p.tpFilled, costRecovered: !!p.costRecovered, bagTrailPct: p.bagTrailPct ?? null, ladder: p.ladder });
182
+ this.say(`ADOPT ${p.symbol} (${p.id}) ${why}`);
183
+ }
184
+ /** A position that left the desk by another path (closed by the user or an agent): stop watching it. */
185
+ forget(mint, why) {
186
+ const p = this.positions.get(mint);
187
+ if (!p)
188
+ return;
189
+ this.positions.delete(mint);
190
+ p.status = 'closed';
191
+ p.closedAt = this.now();
192
+ p.exitReason = why;
193
+ this.closed.push(p);
194
+ this.ledger.add(this.now(), 'position_gone', { positionId: p.id, mint, why });
195
+ }
196
+ /** One market tick. Mechanical actions first (each sell executed at once), then this tick's wakes as ONE message. */
197
+ tick(k) { return this.serial(() => this.tickNow(k)); }
198
+ async tickNow(k) {
199
+ await this.expire();
200
+ const p = this.positions.get(k.mint);
201
+ if (!p || p.status !== 'open')
202
+ return;
203
+ let prof = this.profileOf(p);
204
+ const x = this.strategy.execution;
205
+ let sellUsd;
206
+ // Cost recovery is sized on desk quotes; with none to be had it waits for the next tick (never a guess).
207
+ if (prof.costRecovery && !p.costRecovered && k.price && pnlPct(p, k.price) >= prof.costRecovery.atPct && (k.liq ?? 0) >= x.minExitLiquidityUsd && !p.stuck) {
208
+ const sized = this.exec.costRecoverySellUsd ? await this.exec.costRecoverySellUsd(p, prof, k) : { sellUsd: (t) => t * k.price, quotes: 0 };
209
+ if (sized) {
210
+ sellUsd = sized.sellUsd;
211
+ this.ledger.add(k.t, 'cost_recovery_sized', { positionId: p.id, quotes: sized.quotes });
212
+ }
213
+ else {
214
+ prof = { ...prof, costRecovery: null };
215
+ this.ledger.add(k.t, 'cost_recovery_deferred', { positionId: p.id, why: 'no quote' });
216
+ }
217
+ }
218
+ const acts = monitorTick(p, prof, k, { minExitLiquidityUsd: x.minExitLiquidityUsd, ...(sellUsd ? { sellUsd } : {}) });
219
+ const wakes = [];
220
+ for (const a of acts) {
221
+ if (a.kind === 'wake') {
222
+ wakes.push(a.wake);
223
+ continue;
224
+ }
225
+ if (p.status !== 'open')
226
+ break;
227
+ if (a.kind === 'sell') {
228
+ const wasStuck = p.stuck;
229
+ const ok = await this.sell(p, k, a.tokens, a.reason, a.detail, a.all);
230
+ if (!ok) {
231
+ // A refused partial sell is retried on the next tick (undo its bookkeeping); a refused full exit marks
232
+ // the bag stuck, and monitorTick retries it every tick.
233
+ if (a.reason === 'cost-recovery')
234
+ p.costRecovered = false;
235
+ const tp = /^tp(\d+)$/.exec(a.reason);
236
+ if (tp)
237
+ p.tpFilled = Math.min(p.tpFilled, Number(tp[1]) - 1);
238
+ if (a.all && !wasStuck)
239
+ wakes.push({ event: 'stuck', positionId: p.id, mint: p.mint, t: k.t, detail: `${a.reason}: the sell could not execute; retried every tick`, allowed: ['hold'] });
240
+ break;
241
+ }
242
+ }
243
+ else if (a.kind === 'add')
244
+ await this.add(p, k, a.usd, a.index, a.detail);
245
+ }
246
+ if (wakes.length)
247
+ await this.deliver(p, wakes, k);
248
+ }
249
+ async sell(p, k, tokens, reason, detail, all, agentReason) {
250
+ if (tokens <= 0)
251
+ return true;
252
+ const f = await this.exec.sell(k, Math.min(tokens, p.tokens), reason, { detail, positionId: p.id, ...(agentReason ? { agentReason } : {}) });
253
+ if ('refused' in f) {
254
+ this.ledger.add(k.t, 'refused', { rule: f.refused, stage: 'exit', positionId: p.id, mint: p.mint, reason });
255
+ if (all && !p.stuck) {
256
+ p.stuck = true;
257
+ this.ledger.add(k.t, 'stuck', { positionId: p.id, mint: p.mint, why: f.refused, reason });
258
+ this.say(`STUCK ${p.symbol}: ${f.refused}`);
259
+ }
260
+ return false;
261
+ }
262
+ p.stuck = false;
263
+ p.tokens = Math.max(0, p.tokens - f.tokens);
264
+ if (all || p.tokens < p.origTokens * 1e-6)
265
+ p.tokens = 0;
266
+ p.realizedUsd += f.usd;
267
+ p.fills.push(f);
268
+ this.ledger.add(k.t, 'fill', { positionId: p.id, mint: p.mint, side: 'sell', usd: f.usd, tokens: r6(f.tokens), price: f.price, reason, detail, executor: this.exec.name });
269
+ this.say(`SELL ${p.symbol} ${reason}: $${f.usd.toFixed(2)} — ${detail}`);
270
+ if (p.tokens <= 0) {
271
+ p.status = 'closed';
272
+ p.closedAt = k.t;
273
+ p.exitReason = reason;
274
+ this.positions.delete(p.mint);
275
+ this.closed.push(p);
276
+ const pnl = p.realizedUsd - p.costUsd;
277
+ this.ledger.add(k.t, 'trade_closed', { positionId: p.id, mint: p.mint, symbol: p.symbol, profile: p.profileId, costUsd: r6(p.costUsd), proceedsUsd: r6(p.realizedUsd), pnlUsd: r6(pnl), pnlPct: Math.round((pnl / p.costUsd) * 1e4) / 100, exitReason: reason });
278
+ // A position that closed by any path settles its open wakes (their late answers are ignored).
279
+ for (const q of this.pending.values())
280
+ if (q.batch.positionId === p.id && !q.settled) {
281
+ q.settled = true;
282
+ this.ledger.add(k.t, 'wake_settled', { batchId: q.batch.id, why: 'position-closed' });
283
+ }
284
+ }
285
+ return true;
286
+ }
287
+ /** A ladder add: mechanical (the user's own strategy), re-capped, and only while the runner is up. */
288
+ async add(p, k, usd, index, detail) {
289
+ const s = this.strategy;
290
+ if (!this.runnerAlive()) {
291
+ this.ledger.add(k.t, 'refused', { rule: 'ladder-add-runner-down', positionId: p.id });
292
+ return;
293
+ }
294
+ const b = this.exec.book?.() ?? null;
295
+ const room = b ? Math.min(usd, b.cashUsd, (b.equityUsd * s.risk.maxExposurePct) / 100 - b.openCostUsd) : usd;
296
+ if (!(room >= s.sizing.minUsd)) {
297
+ this.ledger.add(k.t, 'refused', { rule: 'ladder-add-no-room', positionId: p.id, usd: r6(usd) });
298
+ return;
299
+ }
300
+ const f = await this.exec.buy(k, room, { kind: 'add', positionId: p.id, profile: p.profileId, sessionId: p.sessionId, reason: detail });
301
+ if ('refused' in f) {
302
+ this.ledger.add(k.t, 'refused', { rule: f.refused, stage: 'ladder-add', positionId: p.id });
303
+ return;
304
+ }
305
+ const tokens = p.tokens + f.tokens;
306
+ p.entryPrice = (p.entryPrice * p.tokens + f.price * f.tokens) / tokens;
307
+ p.tokens = tokens;
308
+ p.origTokens += f.tokens;
309
+ p.costUsd += f.usd;
310
+ p.fills.push(f);
311
+ this.ledger.add(k.t, 'fill', { positionId: p.id, mint: p.mint, side: 'buy', usd: f.usd, tokens: r6(f.tokens), ...withMcap({ price: f.price }, { price: 'mcap' }, tickHints(k)), reason: 'ladder-add', index, executor: this.exec.name }); // task-1537: JSON keeps price and mcap
312
+ }
313
+ // ───────────────────────────── wakes ─────────────────────────────
314
+ async deliver(p, events, k) {
315
+ const t = this.now();
316
+ const allowed = [...new Set(events.flatMap((w) => w.allowed))];
317
+ const text = events.map((w) => renderEvent(w, p, k, this.basis)).join('\n\n');
318
+ const batch = { id: `w${++this.batchSeq}-${t.toString(36)}`, positionId: p.id, mint: p.mint, t: k.t, events, text, allowed, deadline: t + this.decideMs };
319
+ for (const w of events)
320
+ this.ledger.add(k.t, 'wake', { positionId: p.id, mint: p.mint, event: w.event, detail: w.detail, batchId: batch.id });
321
+ this.say(`WAKE ${p.symbol} ${events.map((w) => w.event).join(' + ')}`);
322
+ const review = events.find((w) => REVIEW.includes(w.event)) ?? null;
323
+ const reply = this.wakes ? this.wakes.deliver(batch, p) : null;
324
+ this.ledger.add(k.t, 'message', { batchId: batch.id, positionId: p.id, events: events.map((w) => w.event), bytes: text.length, session: reply !== null });
325
+ if (reply === null) {
326
+ if (review)
327
+ await this.noAnswerNow(p, review, 'no session');
328
+ return;
329
+ }
330
+ const q = { batch, review, settled: false };
331
+ this.pending.set(batch.id, q);
332
+ reply.then((d) => this.serial(() => this.onReply(q, d)), () => this.serial(() => this.onReply(q, null)));
333
+ }
334
+ /** Wakes past their deadline: the profile's noAnswer for a review, hold for the rest. Runs at every tick. */
335
+ async expire() {
336
+ const now = this.now();
337
+ for (const q of [...this.pending.values()]) {
338
+ if (q.settled || now < q.batch.deadline)
339
+ continue;
340
+ q.settled = true;
341
+ this.pending.delete(q.batch.id);
342
+ if (this.wakes instanceof MailboxWakeSink)
343
+ this.wakes.drop(q.batch.id);
344
+ this.ledger.add(now, 'session_timeout', { batchId: q.batch.id, positionId: q.batch.positionId });
345
+ const p = [...this.positions.values()].find((x) => x.id === q.batch.positionId);
346
+ if (p && q.review)
347
+ await this.noAnswerNow(p, q.review, 'timeout');
348
+ else if (p)
349
+ this.ledger.add(now, 'no_answer', { positionId: p.id, events: q.batch.events.map((w) => w.event), why: 'timeout', rule: 'hold' });
350
+ }
351
+ }
352
+ /** Check deadlines without a tick (the process loop calls this every cycle). */
353
+ sweep() { return this.serial(() => this.expire()); }
354
+ get executorName() { return this.exec.name; }
355
+ async noAnswerNow(p, w, why) {
356
+ const rule = this.profileOf(p).wake?.noAnswer ?? 'hold';
357
+ this.ledger.add(this.now(), 'no_answer', { positionId: p.id, event: w.event, why, rule });
358
+ if (rule === 'exit' && p.status === 'open')
359
+ await this.sell(p, p.lastTick, p.tokens, `${w.event}:no-answer`, `${w.event} unanswered (${why}); the profile says exit`, true);
360
+ }
361
+ async onReply(q, d) {
362
+ const now = this.now();
363
+ if (q.settled) {
364
+ this.ledger.add(now, 'late_reply_ignored', { batchId: q.batch.id, positionId: q.batch.positionId, decision: d });
365
+ return;
366
+ }
367
+ q.settled = true;
368
+ this.pending.delete(q.batch.id);
369
+ const p = [...this.positions.values()].find((x) => x.id === q.batch.positionId);
370
+ if (!p) {
371
+ this.ledger.add(now, 'late_reply_ignored', { batchId: q.batch.id, positionId: q.batch.positionId, decision: d, why: 'position-closed' });
372
+ return;
373
+ }
374
+ if (!d) {
375
+ this.ledger.add(now, 'no_answer', { positionId: p.id, events: q.batch.events.map((w) => w.event), why: 'no reply', rule: q.review ? (this.profileOf(p).wake?.noAnswer ?? 'hold') : 'hold' });
376
+ if (q.review)
377
+ await this.noAnswerNow(p, q.review, 'no reply');
378
+ return;
379
+ }
380
+ this.ledger.add(now, 'reply', { batchId: q.batch.id, positionId: p.id, decision: d });
381
+ await this.apply(p, d, q.batch.allowed);
382
+ }
383
+ /**
384
+ * Apply a session's decision on an open position. Risk can only go down: exit / trim always, tighten (arm the
385
+ * trail now, or halve a free bag's trail, never loosen), re-profile only along an allowed edge (an upgrade needs
386
+ * TP1). Anything that would add size is refused and logged; nothing here can stop a mechanical exit.
387
+ */
388
+ async apply(p, d, allowed) {
389
+ const now = this.now();
390
+ const refuse = (rule, extra = {}) => { this.ledger.add(now, 'refused', { rule, stage: 'wake-reply', positionId: p.id, action: d.action, ...extra }); };
391
+ if (d.action === 'hold' || d.action === 'wait')
392
+ return;
393
+ if (d.action === 'enter' || d.action === 'skip') {
394
+ refuse(d.action === 'enter' ? 'session-cannot-add-size' : 'action-not-allowed-while-holding');
395
+ return;
396
+ }
397
+ if (!allowed.includes(d.action)) {
398
+ refuse('action-not-offered', { allowed });
399
+ return;
400
+ }
401
+ const prof = this.profileOf(p);
402
+ if (d.action === 'exit') {
403
+ const frac = Math.min(1, Math.max(0, d.fraction ?? 1));
404
+ if (!(frac > 0)) {
405
+ refuse('bad-fraction');
406
+ return;
407
+ }
408
+ const all = frac >= 0.999;
409
+ await this.sell(p, p.lastTick, all ? p.tokens : p.tokens * frac, all ? 'agent-exit' : 'agent-trim', d.reason.slice(0, 200), all, d.reason);
410
+ return;
411
+ }
412
+ if (d.action === 'tighten') {
413
+ if (prof.bagTrail && p.costRecovered) {
414
+ const before = p.bagTrailPct ?? prof.bagTrail.trailPct;
415
+ p.bagTrailPct = Math.max(10, Math.min(before, Math.round(prof.bagTrail.trailPct / 2)));
416
+ this.ledger.add(now, 'tighten', { positionId: p.id, armed: 'bag-trail', trailPct: p.bagTrailPct, from: before });
417
+ }
418
+ else if (prof.trailingStop) {
419
+ p.trailArmed = true;
420
+ this.ledger.add(now, 'tighten', { positionId: p.id, armed: 'trail' });
421
+ }
422
+ else
423
+ refuse('tighten-no-trail');
424
+ return;
425
+ }
426
+ if (d.action === 'reprofile') {
427
+ const to = d.profile ? this.strategy.exits.profiles[d.profile] : undefined;
428
+ if (!to || !prof.reprofileTo.includes(to.id)) {
429
+ refuse('reprofile-edge', { to: d.profile ?? null });
430
+ return;
431
+ }
432
+ if (to.sizing.maxSizePctOfBankroll > prof.sizing.maxSizePctOfBankroll && p.tpFilled < 1) {
433
+ refuse('reprofile-upgrade-needs-tp1', { to: to.id });
434
+ return;
435
+ }
436
+ this.ledger.add(now, 'reprofile', { positionId: p.id, from: prof.id, to: to.id, reason: d.reason });
437
+ p.profileId = to.id;
438
+ }
439
+ }
440
+ /** Wait for every queued tick and reply to finish (tests and shutdown). */
441
+ async idle() {
442
+ // Let settled replies queue their apply step first (a resolved reply joins the chain one turn later).
443
+ await new Promise((res) => setImmediate(res));
444
+ await this.serial(async () => { });
445
+ }
446
+ /** The status the heartbeat file carries (and recovery reads back for peaks and windows). */
447
+ status() {
448
+ return [...this.positions.values()].map((p) => ({
449
+ positionId: p.id, mint: p.mint, symbol: p.symbol, profile: p.profileId, tokens: r6(p.tokens), costUsd: r6(p.costUsd), realizedUsd: r6(p.realizedUsd),
450
+ peakPrice: p.peakPrice, entryPrice: p.entryPrice, lastPrice: p.lastTick.price, lastT: p.lastTick.t, tpFilled: p.tpFilled, costRecovered: !!p.costRecovered,
451
+ bagTrailPct: p.bagTrailPct ?? null, trailArmed: p.trailArmed, stuck: p.stuck, liqWindow: p.liqWindow.slice(-20), lowTapeSince: p.lowTapeSince, wakeSent: { ...p.wakeSent },
452
+ pnlPct: p.lastTick.price ? Math.round(pnlPct(p, p.lastTick.price) * 10) / 10 : null,
453
+ }));
454
+ }
455
+ pendingWakes() { return [...this.pending.values()].filter((q) => !q.settled).length; }
456
+ }
457
+ /** Rebuild the open positions from the paper journal + run ledgers (+ the last checkpoint). Pure. */
458
+ export function recoverPositions(r) {
459
+ const owner = r.owner ?? 'autotrade-monitor';
460
+ const plans = new Map();
461
+ const fills = new Map();
462
+ const exits = new Map();
463
+ for (const e of r.journal) {
464
+ const d = (e.data ?? {});
465
+ if (e.type === 'agent.paper.exit.plan') {
466
+ if (d.owner !== owner || typeof d.mint !== 'string' || typeof d.positionId !== 'string')
467
+ continue;
468
+ plans.set(d.mint, { ...d, seq: e.seq });
469
+ }
470
+ else if (e.type === 'agent.paper.exit.done') {
471
+ const pl = typeof d.mint === 'string' ? plans.get(d.mint) : undefined;
472
+ if (pl && pl.positionId === d.positionId)
473
+ plans.delete(d.mint);
474
+ }
475
+ else if (e.type === 'agent.paper.fill') {
476
+ const note = (d.note ?? null);
477
+ const pid = note && typeof note.positionId === 'string' ? note.positionId : null;
478
+ if (!pid)
479
+ continue;
480
+ const list = fills.get(pid) ?? [];
481
+ list.push({ ts: e.ts, side: d.side === 'sell' ? 'sell' : 'buy', solDelta: Number(d.solDelta), tokenDelta: Number(d.tokenDelta), priceSol: Number(d.priceSol), priceUsd: typeof d.priceUsd === 'number' ? d.priceUsd : null, mid: typeof note.mid === 'number' && note.mid > 0 ? note.mid : null, exit: typeof note.exit === 'string' ? note.exit : null, seq: e.seq });
482
+ fills.set(pid, list);
483
+ }
484
+ else if (e.type.startsWith('agent.paper.exit.')) {
485
+ if (typeof d.positionId !== 'string')
486
+ continue;
487
+ const list = exits.get(d.positionId) ?? [];
488
+ list.push({ rule: e.type.slice('agent.paper.exit.'.length), seq: e.seq });
489
+ exits.set(d.positionId, list);
490
+ }
491
+ }
492
+ const held = new Map(r.held.filter((h) => h.tokens > 0).map((h) => [h.mint, h]));
493
+ const ck = new Map((r.checkpoint?.positions ?? []).map((c) => [c.positionId, c]));
494
+ const out = { positions: [], closedWhileDown: [], notOurs: [], unresolvedExits: [] };
495
+ for (const [mint] of held)
496
+ if (!plans.has(mint))
497
+ out.notOurs.push(mint);
498
+ const now = r.now ?? Date.now();
499
+ for (const [mint, plan] of plans) {
500
+ const h = held.get(mint);
501
+ if (!h) {
502
+ out.closedWhileDown.push({ positionId: plan.positionId, mint });
503
+ continue;
504
+ }
505
+ const fl = (fills.get(plan.positionId) ?? []).sort((a, b) => a.seq - b.seq);
506
+ const usdOf = (f) => (Math.abs(f.solDelta) / 1e9) * (f.priceUsd && f.priceSol ? f.priceUsd / f.priceSol : r.solUsd);
507
+ const tokOf = (f) => Math.abs(f.tokenDelta) / 10 ** h.decimals;
508
+ const buys = fl.filter((f) => f.side === 'buy');
509
+ const sells = fl.filter((f) => f.side === 'sell');
510
+ const costUsd = buys.reduce((a, f) => a + usdOf(f), 0) || h.costSol * r.solUsd;
511
+ const origTokens = buys.reduce((a, f) => a + tokOf(f), 0) || h.tokens;
512
+ // The tick's mid price the entry was decided on (the note carries it), else the fill's own price.
513
+ const midOf = (f) => f.mid ?? f.priceUsd ?? f.priceSol * r.solUsd;
514
+ const entryPrice = buys.length ? buys.reduce((a, f) => a + midOf(f) * tokOf(f), 0) / origTokens : costUsd / origTokens;
515
+ const fillsE = fl.map((f) => ({ t: f.ts, side: f.side, usd: r6(usdOf(f)), tokens: tokOf(f), price: midOf(f), avgPrice: tokOf(f) ? usdOf(f) / tokOf(f) : 0, impactPct: 0, partial: false, reason: f.exit ?? (f.side === 'buy' ? 'entry' : 'sell') }));
516
+ let tpFilled = 0;
517
+ for (const f of sells) {
518
+ const m = /^tp(\d+)$/.exec(f.exit ?? '');
519
+ if (m)
520
+ tpFilled = Math.max(tpFilled, Number(m[1]));
521
+ }
522
+ const filledExits = new Set(sells.map((f) => f.exit));
523
+ // An exit line whose fill never followed (a crash between the journal line and the booking): reported, not re-sent.
524
+ const ex = exits.get(plan.positionId) ?? [];
525
+ for (const x of ex)
526
+ if (!fl.some((f) => f.side === 'sell' && f.seq > x.seq && f.exit === x.rule))
527
+ out.unresolvedExits.push({ positionId: plan.positionId, rule: x.rule, seq: x.seq });
528
+ const c = ck.get(plan.positionId);
529
+ let profileId = plan.profileId;
530
+ let bagTrailPct = c?.bagTrailPct ?? undefined;
531
+ let trailArmed = c?.trailArmed ?? false;
532
+ const wakeSent = { ...(c?.wakeSent ?? {}) };
533
+ let ladder = [];
534
+ for (const lg of r.ledgers ?? []) {
535
+ for (const e of lg) {
536
+ if (e.positionId !== plan.positionId)
537
+ continue;
538
+ if (e.kind === 'reprofile' && typeof e.to === 'string')
539
+ profileId = e.to;
540
+ else if (e.kind === 'tighten' && e.armed === 'bag-trail' && typeof e.trailPct === 'number')
541
+ bagTrailPct = Math.min(bagTrailPct ?? Infinity, e.trailPct);
542
+ else if (e.kind === 'tighten' && e.armed === 'trail')
543
+ trailArmed = true;
544
+ else if (e.kind === 'wake' && typeof e.event === 'string') {
545
+ const key = e.event === 'time_review' || e.event === 'time_tighten' ? 'time' : e.event === 'drawdown' ? 'drawdown' : e.event === 'thesis_warning' ? 'thesis' : null;
546
+ if (key)
547
+ wakeSent[key] = true;
548
+ }
549
+ else if ((e.kind === 'adopt' || e.kind === 'fill') && Array.isArray(e.ladder) && !ladder.length)
550
+ ladder = e.ladder.map((t) => ({ ...t }));
551
+ else if (e.kind === 'fill' && e.reason === 'ladder-add' && typeof e.index === 'number' && ladder[e.index])
552
+ ladder[e.index].usd = 0;
553
+ }
554
+ }
555
+ if (!r.strategy.exits.profiles[profileId])
556
+ profileId = r.strategy.exits.default;
557
+ const openedAt = buys[0]?.ts ?? plan.registeredAt;
558
+ const lastPrice = c?.lastPrice ?? entryPrice;
559
+ out.positions.push({
560
+ id: plan.positionId, mint, symbol: h.symbol, profileId, openedAt, sessionId: null,
561
+ tokens: h.tokens, origTokens, costUsd, realizedUsd: sells.reduce((a, f) => a + usdOf(f), 0), entryPrice,
562
+ peakPrice: Math.max(entryPrice, c?.peakPrice ?? 0, ...fillsE.map((f) => f.price)), tpFilled, trailArmed,
563
+ costRecovered: filledExits.has('cost-recovery') || (c?.costRecovered ?? false), ...(bagTrailPct !== undefined ? { bagTrailPct } : {}),
564
+ ladder, fills: fillsE, status: 'open', stuck: false, closedAt: null, exitReason: null,
565
+ liqWindow: c?.liqWindow?.map((w) => ({ ...w })) ?? [], lowTapeSince: c?.lowTapeSince ?? null, lastTick: bareTick(mint, h.symbol, c?.lastT ?? now, lastPrice), wakeSent,
566
+ });
567
+ }
568
+ return out;
569
+ }
570
+ /**
571
+ * Adopt every live autotrade plan in the paper journal that the monitor does not hold yet, and forget positions
572
+ * whose plan is done or whose mint left the desk by another path. The same code path as a restart.
573
+ */
574
+ export function syncFromJournal(m, input) {
575
+ const r = recoverPositions({ ...input, strategy: m.strategy, ledgers: [...(input.ledgers ?? []), m.ledger.events] });
576
+ const live = new Set(r.positions.map((p) => p.mint));
577
+ for (const p of r.positions)
578
+ if (!m.positions.has(p.mint))
579
+ m.adopt(p, 'journal');
580
+ for (const mint of [...m.positions.keys()])
581
+ if (!live.has(mint))
582
+ m.forget(mint, 'plan-done-or-sold-elsewhere');
583
+ return r;
584
+ }
585
+ const pidAlive = (pid) => { try {
586
+ process.kill(pid, 0);
587
+ return true;
588
+ }
589
+ catch (e) {
590
+ return e.code === 'EPERM';
591
+ } };
592
+ /** One monitor per machine: a lock holding the pid; a lock whose pid is gone is reclaimed. */
593
+ export function acquireMonitorLock(home, now = Date.now()) {
594
+ const path = monitorLockPath(home);
595
+ mkdirSync(join(home, 'autotrade'), { recursive: true, mode: 0o700 });
596
+ let reclaimed = null;
597
+ for (let i = 0; i < 2; i++) {
598
+ try {
599
+ const fd = openSync(path, 'wx', 0o600);
600
+ writeSync(fd, `${JSON.stringify({ pid: process.pid, startedAt: now })}\n`);
601
+ closeSync(fd);
602
+ return { ok: true, reclaimed };
603
+ }
604
+ catch (e) {
605
+ if (e.code !== 'EEXIST')
606
+ throw e;
607
+ }
608
+ let held = null;
609
+ try {
610
+ held = JSON.parse(readFileSync(path, 'utf8'));
611
+ }
612
+ catch {
613
+ held = null;
614
+ }
615
+ if (held && typeof held.pid === 'number' && held.pid !== process.pid && pidAlive(held.pid))
616
+ return { ok: false, holder: held };
617
+ reclaimed = held;
618
+ rmSync(path, { force: true });
619
+ }
620
+ throw new Error(`could not take ${path}`);
621
+ }
622
+ export function releaseMonitorLock(home) {
623
+ try {
624
+ const held = JSON.parse(readFileSync(monitorLockPath(home), 'utf8'));
625
+ if (held.pid === process.pid)
626
+ rmSync(monitorLockPath(home), { force: true });
627
+ }
628
+ catch { /* gone */ }
629
+ }
630
+ export function writeMonitorStatus(home, s) {
631
+ const path = monitorStatusPath(home);
632
+ mkdirSync(join(home, 'autotrade'), { recursive: true, mode: 0o700 });
633
+ const tmp = `${path}.${process.pid}.tmp`;
634
+ writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, { mode: 0o600 });
635
+ renameSync(tmp, path);
636
+ }
637
+ export function readMonitorStatus(home) {
638
+ try {
639
+ const d = JSON.parse(readFileSync(monitorStatusPath(home), 'utf8'));
640
+ return d.schema === MONITOR_STATUS_SCHEMA && d.mode === 'paper' ? d : null;
641
+ }
642
+ catch {
643
+ return null;
644
+ }
645
+ }
646
+ /** A hash-chained monitor ledger appended to autotrade/monitor/<id>.jsonl (0600) as it happens. */
647
+ export function monitorFileLedger(home, id) {
648
+ if (!/^[0-9A-Za-z-]{1,64}$/.test(id))
649
+ throw new Error(`not a monitor ledger id: ${JSON.stringify(id)}`);
650
+ mkdirSync(monitorLedgerDir(home), { recursive: true, mode: 0o700 });
651
+ const path = join(monitorLedgerDir(home), `${id}.jsonl`);
652
+ if (existsSync(path))
653
+ throw new Error(`monitor ledger ${path} already exists`);
654
+ closeSync(openSync(path, 'a', 0o600));
655
+ return { ledger: new Ledger((_e, line) => appendFileSync(path, `${line}\n`)), path };
656
+ }
657
+ /**
658
+ * The monitor process body: sync positions from the journal → tick every held coin → settle wakes → heartbeat,
659
+ * every `cycleMs` (5 s, the fast lane's cadence). It never pauses for a guard: only a stop ends it. On a clean
660
+ * stop it writes `state: stopped`, so the desk refuses entries at once instead of after the stale window.
661
+ */
662
+ export async function runMonitorProcess(o) {
663
+ const now = o.now ?? Date.now;
664
+ const cycleMs = o.cycleMs ?? 5_000;
665
+ // A ref'd timer keeps node alive while the loop sleeps (an unref'd one lets the process exit mid-run).
666
+ const sleep = o.sleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
667
+ const startedAt = now();
668
+ const m = o.monitor;
669
+ let lastBeat = 0;
670
+ let cycles = 0;
671
+ const beat = (state) => {
672
+ writeMonitorStatus(o.home, {
673
+ schema: MONITOR_STATUS_SCHEMA, mode: 'paper', pid: process.pid, state, startedAt, heartbeatAt: now(),
674
+ strategy: { id: m.strategy.id, version: m.strategy.version, hash: m.strategy.hash }, executor: m.executorName,
675
+ runnerAlive: o.runnerAlive?.() ?? false, positions: m.status(), pendingWakes: m.pendingWakes(), ledgerPath: o.ledgerPath ?? null, ledgerHead: m.ledger.head(),
676
+ });
677
+ lastBeat = now();
678
+ };
679
+ beat('running');
680
+ try {
681
+ while (!(o.shouldStop?.() ?? false) && (o.maxCycles === undefined || cycles < o.maxCycles)) {
682
+ cycles++;
683
+ try {
684
+ await o.sync?.();
685
+ }
686
+ catch (e) {
687
+ m.ledger.add(now(), 'sync_failed', { error: e instanceof Error ? e.message.slice(0, 200) : String(e) });
688
+ }
689
+ const mints = [...m.positions.keys()];
690
+ if (mints.length) {
691
+ let ticks = [];
692
+ try {
693
+ ticks = await o.ticks(mints);
694
+ }
695
+ catch (e) {
696
+ m.ledger.add(now(), 'ticks_failed', { error: e instanceof Error ? e.message.slice(0, 200) : String(e) });
697
+ }
698
+ for (const k of ticks)
699
+ await m.tick(k);
700
+ }
701
+ o.mailbox?.poll();
702
+ await m.sweep();
703
+ if (now() - lastBeat >= MONITOR_HEARTBEAT_MS || cycles === 1)
704
+ beat('running');
705
+ if (o.maxCycles !== undefined && cycles >= o.maxCycles)
706
+ break;
707
+ await sleep(cycleMs);
708
+ }
709
+ }
710
+ finally {
711
+ await m.idle();
712
+ beat('stopped');
713
+ }
714
+ return { cycles };
715
+ }