dexterm-cli 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/dist/packages/agents/src/schema.js +22 -1
  2. package/dist/packages/agents/src/spectator.js +3 -1
  3. package/dist/packages/agents/src/story.js +11 -2
  4. package/dist/packages/agents/src/tiers.js +36 -2
  5. package/dist/packages/agents/src/tools.js +35 -2
  6. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
  7. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  8. package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
  9. package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
  10. package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
  11. package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
  12. package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
  13. package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
  14. package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
  15. package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
  16. package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
  17. package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
  18. package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
  19. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  20. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  21. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  22. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  23. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  24. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  25. package/dist/packages/autotrade/src/account.js +74 -0
  26. package/dist/packages/autotrade/src/backtest.js +358 -0
  27. package/dist/packages/autotrade/src/basis-text.js +32 -0
  28. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  29. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  30. package/dist/packages/autotrade/src/chat/config.js +95 -0
  31. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  32. package/dist/packages/autotrade/src/chat/control.js +104 -0
  33. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  34. package/dist/packages/autotrade/src/chat/format.js +71 -0
  35. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  36. package/dist/packages/autotrade/src/chat/index.js +18 -0
  37. package/dist/packages/autotrade/src/chat/notify.js +302 -0
  38. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  39. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  40. package/dist/packages/autotrade/src/chat/secrets.js +106 -0
  41. package/dist/packages/autotrade/src/chat/service.js +251 -0
  42. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  43. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  44. package/dist/packages/autotrade/src/engine.js +401 -0
  45. package/dist/packages/autotrade/src/execution-port.js +97 -0
  46. package/dist/packages/autotrade/src/executor-health.js +217 -0
  47. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  48. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  49. package/dist/packages/autotrade/src/fleet.js +642 -0
  50. package/dist/packages/autotrade/src/forward.js +355 -0
  51. package/dist/packages/autotrade/src/index.js +72 -0
  52. package/dist/packages/autotrade/src/journey.js +477 -0
  53. package/dist/packages/autotrade/src/ledger.js +108 -0
  54. package/dist/packages/autotrade/src/limits.js +682 -0
  55. package/dist/packages/autotrade/src/live-source.js +182 -0
  56. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  57. package/dist/packages/autotrade/src/monitor.js +190 -0
  58. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  59. package/dist/packages/autotrade/src/outbox.js +394 -0
  60. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  61. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  62. package/dist/packages/autotrade/src/real-mode.js +688 -0
  63. package/dist/packages/autotrade/src/receipts.js +330 -0
  64. package/dist/packages/autotrade/src/replay.js +198 -0
  65. package/dist/packages/autotrade/src/report.js +168 -0
  66. package/dist/packages/autotrade/src/runner-config.js +99 -0
  67. package/dist/packages/autotrade/src/runner.js +923 -0
  68. package/dist/packages/autotrade/src/safety.js +243 -0
  69. package/dist/packages/autotrade/src/service-guard.js +125 -0
  70. package/dist/packages/autotrade/src/service-install.js +213 -0
  71. package/dist/packages/autotrade/src/service-recover.js +232 -0
  72. package/dist/packages/autotrade/src/service.js +289 -0
  73. package/dist/packages/autotrade/src/session.js +269 -0
  74. package/dist/packages/autotrade/src/sessions.js +503 -0
  75. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  76. package/dist/packages/autotrade/src/signal.js +196 -0
  77. package/dist/packages/autotrade/src/store.js +250 -0
  78. package/dist/packages/autotrade/src/strategy.js +258 -0
  79. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  80. package/dist/packages/autotrade/src/yolo.js +140 -0
  81. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  82. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  83. package/dist/packages/cli/src/ai-cmd.js +412 -0
  84. package/dist/packages/cli/src/analyst/backend.js +83 -15
  85. package/dist/packages/cli/src/analyst/codex.js +15 -9
  86. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  87. package/dist/packages/cli/src/analyst/session.js +5 -2
  88. package/dist/packages/cli/src/app-help-gen.js +56 -8
  89. package/dist/packages/cli/src/autotrade-cmd/audit.js +74 -0
  90. package/dist/packages/cli/src/autotrade-cmd/backtest.js +216 -0
  91. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  92. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  93. package/dist/packages/cli/src/autotrade-cmd/fleet.js +401 -0
  94. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  95. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  96. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  97. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  98. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  99. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  100. package/dist/packages/cli/src/autotrade-cmd/run.js +657 -0
  101. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  102. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  103. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  104. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  105. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  106. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  107. package/dist/packages/cli/src/autotrade-cmd.js +214 -0
  108. package/dist/packages/cli/src/display.js +156 -17
  109. package/dist/packages/cli/src/doctor.js +62 -3
  110. package/dist/packages/cli/src/export.js +10 -2
  111. package/dist/packages/cli/src/index.js +1 -1
  112. package/dist/packages/cli/src/ledger-cmd.js +290 -0
  113. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  114. package/dist/packages/cli/src/live/index.js +23 -89
  115. package/dist/packages/cli/src/live/screens/token-live.js +9 -6
  116. package/dist/packages/cli/src/live/trade.js +12 -457
  117. package/dist/packages/cli/src/live/watchlist.js +1 -1
  118. package/dist/packages/cli/src/main.js +147 -190
  119. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  120. package/dist/packages/cli/src/plan-cmd.js +1 -1
  121. package/dist/packages/cli/src/pnl-app.js +31 -21
  122. package/dist/packages/cli/src/pnl-card.js +162 -21
  123. package/dist/packages/cli/src/popup.js +22 -6
  124. package/dist/packages/cli/src/record-cmd.js +141 -20
  125. package/dist/packages/cli/src/screens/agent-hud.js +34 -2
  126. package/dist/packages/cli/src/screens/analyst-session.js +7 -4
  127. package/dist/packages/cli/src/screens/analyst.js +3 -2
  128. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  129. package/dist/packages/cli/src/screens/autotrade-keys.js +182 -0
  130. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  131. package/dist/packages/cli/src/screens/autotrade-screen.js +776 -0
  132. package/dist/packages/cli/src/screens/autotrade-ui.js +650 -0
  133. package/dist/packages/cli/src/screens/autotrade-worker.js +91 -0
  134. package/dist/packages/cli/src/screens/brain.js +37 -11
  135. package/dist/packages/cli/src/screens/overlay.js +5 -2
  136. package/dist/packages/cli/src/screens/plans.js +5 -17
  137. package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
  138. package/dist/packages/cli/src/screens/positions.js +6 -28
  139. package/dist/packages/cli/src/screens/ticket.js +38 -205
  140. package/dist/packages/cli/src/screens/token.js +3 -3
  141. package/dist/packages/cli/src/state.js +25 -19
  142. package/dist/packages/cli/src/telegram-cmd.js +454 -0
  143. package/dist/packages/cli/src/trade.js +3 -128
  144. package/dist/packages/cli/src/version.js +1 -1
  145. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  146. package/dist/packages/cli/src/wallet-command.js +8 -11
  147. package/dist/packages/cli/src/widgets.js +0 -12
  148. package/dist/packages/custom/src/config.js +20 -2
  149. package/dist/packages/custom/src/schema.js +35 -10
  150. package/dist/packages/custom/src/workspace.js +3 -3
  151. package/dist/packages/data/src/basis.js +191 -0
  152. package/dist/packages/data/src/detail.js +53 -2
  153. package/dist/packages/data/src/index.js +0 -14
  154. package/dist/packages/data/src/store.js +14 -5
  155. package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
  156. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  157. package/dist/packages/data/src/trade/bridge.js +4 -102
  158. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  159. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  160. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  161. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  162. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  163. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  164. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  165. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  166. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  167. package/dist/packages/data/src/trade/plans/index.js +0 -1
  168. package/dist/packages/data/src/trade/plans/service.js +3 -27
  169. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  170. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  171. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  172. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  173. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  174. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  175. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  176. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  177. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  178. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  179. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  180. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  181. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  182. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  183. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  184. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  185. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  186. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +229 -0
  187. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  188. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  189. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  190. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  191. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  192. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  193. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  194. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  195. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  196. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
  198. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  200. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  201. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +51 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +38 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +37 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +50 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  231. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  232. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  233. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  235. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  236. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  237. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  240. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  242. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
  243. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  244. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  246. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
  247. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  248. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  249. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  250. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  251. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  254. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  255. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  256. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  257. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +31 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  271. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  273. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  274. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  275. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  277. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  278. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  281. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  282. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  284. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  288. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  289. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
  290. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  291. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
  292. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  293. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  295. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  297. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  298. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  299. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
  301. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  302. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  304. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  305. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  306. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
  307. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  308. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  309. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  310. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  311. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  312. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  313. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
  314. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  315. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  316. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  317. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  318. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  319. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  320. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  321. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  322. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  323. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  324. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  325. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  326. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  327. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  328. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  329. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  330. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  331. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  332. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  333. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  334. package/dist/packages/mcp/assets/kb/entries/autotrade.md +40 -0
  335. package/dist/packages/mcp/src/app-help.js +1 -1
  336. package/dist/packages/mcp/src/autotrade-tools.js +1107 -0
  337. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  338. package/dist/packages/mcp/src/engine/config.js +159 -0
  339. package/dist/packages/mcp/src/engine/index.js +10 -0
  340. package/dist/packages/mcp/src/engine/loop.js +101 -0
  341. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  342. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  343. package/dist/packages/mcp/src/engine/probe.js +110 -0
  344. package/dist/packages/mcp/src/engine/reason.js +35 -0
  345. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  346. package/dist/packages/mcp/src/index.js +7 -1
  347. package/dist/packages/mcp/src/ledger-tools.js +494 -0
  348. package/dist/packages/mcp/src/paper-agent.js +2 -0
  349. package/dist/packages/mcp/src/server.js +138 -19
  350. package/dist/packages/packs/src/backtest.js +3 -1
  351. package/dist/packages/popup/src/pages/chart-lib.js +134 -21
  352. package/dist/packages/popup/src/pages/chart.css +9 -0
  353. package/dist/packages/popup/src/pages/chart.html +3 -1
  354. package/dist/packages/popup/src/pages/chart.js +104 -7
  355. package/dist/packages/popup/src/protocol.js +2 -0
  356. package/dist/packages/popup/src/server.js +4 -0
  357. package/dist/packages/recorder/src/index.js +2 -0
  358. package/dist/packages/recorder/src/recorder.js +63 -6
  359. package/dist/packages/recorder/src/retention.js +100 -0
  360. package/dist/packages/recorder/src/schema.js +34 -1
  361. package/dist/packages/recorder/src/ticks.js +237 -0
  362. package/dist/packages/tui/src/index.js +1 -0
  363. package/dist/packages/tui/src/keymap.js +28 -1
  364. package/dist/packages/tui/src/markers.js +219 -0
  365. package/dist/packages/tui/src/widgets.js +149 -2
  366. package/package.json +2 -1
  367. package/dist/packages/data/src/trade/caps.js +0 -323
  368. package/dist/packages/data/src/trade/executor.js +0 -933
  369. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  370. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  371. package/dist/packages/data/src/trade/signer/client.js +0 -197
  372. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  373. package/dist/packages/data/src/trade/signer/host.js +0 -97
  374. package/dist/packages/data/src/trade/signer/main.js +0 -121
  375. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  376. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  377. package/dist/packages/data/src/trade/signer/service.js +0 -400
  378. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  379. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  380. package/dist/packages/data/src/trade/swap-build.js +0 -108
  381. package/dist/packages/data/src/trade/token-delta.js +0 -201
  382. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  383. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  384. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  385. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  386. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  387. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  388. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  389. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  390. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  391. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  392. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  393. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  394. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  395. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // dexterm executor reference kit: the --signal-cmd hook (task-1654). DRY RUN: logs what it would do; no wallet code.
3
+ //
4
+ // dexterm runs it once per signal, with no shell, the signal line on stdin and the HMAC in DEXTERM_SIGNAL_SIGNATURE
5
+ // (when a secret is set). Wire it with an absolute path:
6
+ // dexterm autotrade signal channels --signal-cmd '["/usr/local/bin/node","/path/to/signal-cmd-executor.mjs","--account","main","--secret-file","/path/to/secret"]'
7
+ //
8
+ // Exit codes dexterm reads: 0 acted (dry run) · 3 duplicate (counted delivered) · 2 rejected or expired (never
9
+ // retried) · anything else is retried. A line per signal goes to --log (default autotrade/real/kit-cmd.log).
10
+
11
+ import { appendFileSync, readFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { SeenStore, args, dextermHome, handle, heartbeat, readSecret, realDir } from './kit.mjs';
14
+
15
+ const a = args();
16
+ if (!a.account) { console.error('usage: signal-cmd-executor.mjs --account ID [--secret-file FILE] [--home DIR]'); process.exit(2); }
17
+ const home = a.home || dextermHome();
18
+ const logFile = a.log || join(realDir(home), 'kit-cmd.log');
19
+ const log = (s) => { try { appendFileSync(logFile, `[${new Date().toISOString()}] ${s}\n`); } catch { /* the exit code still answers */ } };
20
+ const name = String(a.name || 'kit-signal-cmd').slice(0, 32);
21
+ const body = readFileSync(0, 'utf8').trim();
22
+ const seen = new SeenStore(a.state || join(realDir(home), 'kit-cmd-seen.json'));
23
+ const c = handle(body, { accountId: a.account, secret: readSecret(a['secret-file']), requireHmac: !!a['require-hmac'], signature: process.env.DEXTERM_SIGNAL_SIGNATURE ?? null, seen, home, executor: name, log, now: Date.now() });
24
+ try { heartbeat(home, { executor: name, accountId: a.account, lastNonce: seen.maxNonce || null }); } catch { /* optional */ }
25
+ process.exit(c.verdict === 'act' ? 0 : c.verdict === 'duplicate' ? 3 : 2);
@@ -0,0 +1,47 @@
1
+ # Recipe: a Telegram trading bot as your executor
2
+
3
+ Many people already trade memecoins through a Telegram trading bot. That bot holds the wallet and signs the swaps.
4
+ dexterm can drive the decision side, but it can never be the bot and never holds the bot's key.
5
+
6
+ ## Why the Telegram copy alone is not enough
7
+
8
+ dexterm's Telegram copy of each signal goes to **your** chat, for you to read. A bot cannot read another bot's
9
+ messages, so your trading bot never sees that copy. Something on your side has to hand each signal to the bot.
10
+
11
+ ## The shape that works
12
+
13
+ ```
14
+ dexterm (real mode) --signal--> your small relay (one of the kit executors) --order--> your trading bot
15
+ <--receipt-- <--fill--
16
+ ```
17
+
18
+ 1. Start from one of the kit executors, for example `webhook-executor.mjs`. It already verifies the HMAC, dedupes
19
+ by nonce, honours the TTL, writes receipts and sends heartbeats.
20
+ 2. Replace `act()` in `kit.mjs` with the call your bot offers to accounts that automate it. Depending on the bot
21
+ that is an HTTP API, a CLI, or a message your own user account sends to it through a Telegram client library.
22
+ Use only what the bot documents and allows. That code, its credentials and the wallet are yours; keep them out
23
+ of dexterm's folders.
24
+ 3. When the bot answers, write the receipt with the real fill: `status: "executed"` plus `priceUsd`, `sizeSol`,
25
+ `tokens` and `txSig`, or `rejected` / `failed` with a reason. dexterm then shows how far the real fills were
26
+ from paper.
27
+ 4. Keep the relay's heartbeat running. If the relay or the bot goes quiet longer than your `executorSilenceMin`,
28
+ dexterm pauses new buy signals and tells you.
29
+
30
+ ## Rules the relay must keep (the kit does)
31
+
32
+ - It acts only on signals for its own `accountId`.
33
+ - It never acts after `expiresAt`, and it never retries a stale order on its own.
34
+ - It dedupes by `nonce` and `id`, because the same signal can arrive twice.
35
+ - It treats `type: "test"` as a test: there is never an order.
36
+ - On `type: "halted"`, it may cancel the `outstanding` nonces that are still open on its side. `open` lists what
37
+ dexterm still counts as held.
38
+ - It sizes from its own wallet. `sizeSol` is what dexterm's limits allowed, and `sellFraction` applies to what the
39
+ bot really holds.
40
+
41
+ ## Before any real money
42
+
43
+ - Run the relay in dry run first. `dexterm autotrade signal test` must show every channel delivered and the relay's
44
+ receipt.
45
+ - Set your own limits (`dexterm autotrade limits set …`). The defaults are placeholders: 0.25 SOL per trade, 1 SOL
46
+ a day, 20 buys, 3 open, stop at -0.5 SOL a day.
47
+ - Keep `dexterm autotrade signal kill` at hand. It stops every signal except one final `halted`.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // dexterm executor reference kit: the WEBHOOK receiver (task-1654). DRY RUN: logs what it would do; no wallet code.
3
+ //
4
+ // node webhook-executor.mjs --account main [--port 8787] [--secret-file FILE] [--home DIR] [--heartbeat-sec 60]
5
+ //
6
+ // Then point dexterm at it: dexterm autotrade signal channels --webhook http://127.0.0.1:8787/signal
7
+ // and prove the wiring: dexterm autotrade signal test
8
+ //
9
+ // Answers: 200 acted (dry run) · 409 duplicate (dexterm counts it delivered) · 410 expired · 401 bad signature ·
10
+ // 400 anything else it will not act on. It binds 127.0.0.1 only. Ctrl-C stops it.
11
+
12
+ import { createServer } from 'node:http';
13
+ import { join } from 'node:path';
14
+ import { SeenStore, args, dextermHome, handle, heartbeat, readSecret, realDir } from './kit.mjs';
15
+
16
+ const a = args();
17
+ if (!a.account) { console.error('usage: node webhook-executor.mjs --account ID [--port 8787] [--secret-file FILE] [--home DIR]'); process.exit(2); }
18
+ const home = a.home || dextermHome();
19
+ const secret = readSecret(a['secret-file']);
20
+ const seen = new SeenStore(a.state || join(realDir(home), 'kit-webhook-seen.json'));
21
+ const name = String(a.name || 'kit-webhook').slice(0, 32);
22
+ const log = (s) => console.log(`[${new Date().toISOString()}] ${s}`);
23
+ const o = { accountId: a.account, secret, requireHmac: !!a['require-hmac'], seen, home, executor: name, log };
24
+
25
+ const server = createServer((req, res) => {
26
+ if (req.method !== 'POST') { res.writeHead(405); res.end('POST only'); return; }
27
+ let body = '';
28
+ req.setEncoding('utf8');
29
+ req.on('data', (c) => { body += c; if (body.length > 64 * 1024) req.destroy(); });
30
+ req.on('end', () => {
31
+ const c = handle(body, { ...o, signature: req.headers['x-dexterm-signature'] ?? null, now: Date.now() });
32
+ const code = c.verdict === 'act' ? 200 : c.verdict === 'duplicate' ? 409 : c.verdict === 'expired' ? 410 : /^signature/.test(c.why ?? '') ? 401 : 400;
33
+ res.writeHead(code, { 'content-type': 'text/plain' });
34
+ res.end(c.verdict);
35
+ });
36
+ });
37
+
38
+ const beat = () => { try { heartbeat(home, { executor: name, accountId: a.account, lastNonce: seen.maxNonce || null }); } catch (e) { log(`heartbeat failed: ${e.message}`); } };
39
+ server.listen(Number(a.port ?? 8787), '127.0.0.1', () => {
40
+ const port = server.address().port;
41
+ log(`dexterm kit webhook executor (DRY RUN) listening on http://127.0.0.1:${port}/signal · account ${a.account} · HMAC ${secret ? 'checked' : 'NOT checked (no --secret-file)'}`);
42
+ beat();
43
+ });
44
+ const timer = setInterval(beat, Math.max(5, Number(a['heartbeat-sec'] ?? 60)) * 1000);
45
+ const stop = () => { clearInterval(timer); server.close(() => process.exit(0)); };
46
+ process.on('SIGINT', stop);
47
+ process.on('SIGTERM', stop);
@@ -0,0 +1,74 @@
1
+ // dexterm autotrade — which Claude account a session runs on (task-1505 AT-3, AUTOTRADE-SPEC §7.3).
2
+ //
3
+ // App code ships the account KINDS and the env-var names only; which account is used is the user's runner.json.
4
+ //
5
+ // native the user's default `claude` login. Nothing is added; inherited key/token variables are removed
6
+ // so a stray ANTHROPIC_API_KEY in the shell cannot silently switch the account.
7
+ // oauth-token a long-lived Claude login token, fetched at spawn from a helper command or a keychain item and
8
+ // handed to `claude` on file descriptor 3 (CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR=3). It is never
9
+ // put in argv, a file, the environment or the ledger.
10
+ // config-dir CLAUDE_CONFIG_DIR=<dir>: a separate Claude config with its own login (an empty one answers
11
+ // "Not logged in"; it never falls back to the native login).
12
+ // api-key a pay-as-you-go key from a helper or the keychain, in the child's environment only.
13
+ //
14
+ // Proof per session: the init event's `apiKeySource` must be one of `expectApiKeySource` for the kind, else the
15
+ // session is ended before its first turn. Measured on the native OAuth login: `none` (task-1498). The other kinds'
16
+ // values are the expected ones (an OAuth token or a config-dir login is not an API key; a key is reported as
17
+ // ANTHROPIC_API_KEY) and can be overridden per machine in runner.json once measured there. The check catches a
18
+ // switch between key and login; it cannot tell two OAuth logins apart (the init event names no account).
19
+ import { execFile } from 'node:child_process';
20
+ export const EXPECTED_API_KEY_SOURCE = {
21
+ native: ['none'],
22
+ 'oauth-token': ['none'],
23
+ 'config-dir': ['none'],
24
+ 'api-key': ['ANTHROPIC_API_KEY'],
25
+ };
26
+ /** Variables that select an account; removed from the child env unless the kind sets them itself. */
27
+ export const ACCOUNT_ENV_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR'];
28
+ export const TOKEN_FD = 3;
29
+ /** Run a helper command (no shell) or read a keychain item; stdout is the secret, trimmed. */
30
+ export const fetchSecret = (src) => new Promise((resolve, reject) => {
31
+ const [bin, args] = 'command' in src
32
+ ? [src.command[0], src.command.slice(1)]
33
+ : ['security', ['find-generic-password', '-w', '-s', src.keychain.service, ...(src.keychain.account ? ['-a', src.keychain.account] : [])]];
34
+ if (!bin) {
35
+ reject(new Error('empty secret command'));
36
+ return;
37
+ }
38
+ execFile(bin, args, { timeout: 10_000, maxBuffer: 64 * 1024, env: process.env }, (err, stdout) => {
39
+ // The helper's stderr and our error text never carry the secret; stdout is never echoed.
40
+ if (err) {
41
+ reject(new Error(`the account's secret source failed (${'command' in src ? 'helper command' : 'keychain item'}: ${err.code ?? 'exit'})`));
42
+ return;
43
+ }
44
+ const v = String(stdout).trim();
45
+ if (!v) {
46
+ reject(new Error('the account\'s secret source printed nothing'));
47
+ return;
48
+ }
49
+ resolve(v);
50
+ });
51
+ });
52
+ /** Build the env (and fd secret) for one `claude` spawn. `base` is the runner's own environment. */
53
+ export async function prepareAccount(cfg, base, fetch = fetchSecret) {
54
+ const env = { ...base };
55
+ for (const k of ACCOUNT_ENV_VARS)
56
+ delete env[k];
57
+ const expectApiKeySource = cfg.expectApiKeySource?.length ? cfg.expectApiKeySource : EXPECTED_API_KEY_SOURCE[cfg.kind];
58
+ switch (cfg.kind) {
59
+ case 'native':
60
+ return { kind: 'native', env, fdSecret: null, expectApiKeySource, label: 'native' };
61
+ case 'config-dir':
62
+ env.CLAUDE_CONFIG_DIR = cfg.dir;
63
+ return { kind: 'config-dir', env, fdSecret: null, expectApiKeySource, label: `config-dir ${cfg.dir}` };
64
+ case 'oauth-token': {
65
+ const token = await fetch(cfg.source);
66
+ env.CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR = String(TOKEN_FD);
67
+ return { kind: 'oauth-token', env, fdSecret: Buffer.from(token, 'utf8'), expectApiKeySource, label: 'oauth-token' };
68
+ }
69
+ case 'api-key': {
70
+ env.ANTHROPIC_API_KEY = await fetch(cfg.source);
71
+ return { kind: 'api-key', env, fdSecret: null, expectApiKeySource, label: 'api-key' };
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,358 @@
1
+ // dexterm autotrade — backtest + parameter sweep (task-1498 spike, AUTOTRADE-SPEC §3).
2
+ //
3
+ // A backtest is the run loop over a recorded tape with the same engine, clamp and monitor a paper run uses
4
+ // (no separate "backtest logic" to drift). The report gives P/L, win rate, drawdown and per-rule attribution
5
+ // (which exit rule closed what, which trigger opened it, which clamp fired), and always prints the limits of
6
+ // the data next to the numbers.
7
+ //
8
+ // Sweeps follow the skill-care methodology: split the tape CHRONOLOGICALLY by first sighting (the newest
9
+ // 30 % of coins is the held-out TEST, fixed before any tuning; no coin is in both halves), tune on TRAIN only,
10
+ // then report TEST for the winner against the unchanged strategy. Below SKILL_MIN_N test trades the verdict
11
+ // is "too few trades" whatever the numbers say.
12
+ //
13
+ // task-1503 (AT-1, spec §3a): the moonbag lives on a rare tail, so the report also carries the per-entry
14
+ // expectancy with a bootstrap 95% interval, the exit-multiple quantiles, an every-candidate evaluation copy
15
+ // (fixed unit size, book caps lifted, so every entry is an independent sample) and runner recall (of the coins
16
+ // that later ran >= Nx, which did the entry rules pass, what refused the rest, which exit closed the entered).
17
+ // Verdict rule 6: no tuning claim unless train and test both have >= SKILL_MIN_N trades AND the held-out net is
18
+ // above 0 (a loser that loses less does not "win").
19
+ import { SKILL_MIN_N, wilson } from '../../packs/src/index.js';
20
+ import { EntryRules } from "./engine.js";
21
+ import { runAutotrade } from "./runner.js";
22
+ import { validateStrategy, round } from "./strategy.js";
23
+ export const DATA_LIMITS = [
24
+ 'Price path = recorder snapshots (~2-min cadence while listed, written on >= 5% moves or every 30 min), not candles or trades: intrabar spikes and dips are invisible, so stops and take-profits fill at the next recorded price.',
25
+ 'Fills are modelled (constant-product impact on half the recorded liquidity + fee + slippage + network fee), not Jupiter quotes.',
26
+ 'Survivorship: only coins the app recorded (on a screen or in a feed) are in the tape; coins nobody saw are absent.',
27
+ 'Rugs: when liquidity falls under the exit floor the bag cannot be sold (stuck) and is valued at 0 unless liquidity returns; a rug between two rows fills at whatever the next row says (often -99%), never at the level a rule wanted.',
28
+ 'Early entries look later than they are: a young pair enters the tape only once a feed lists it, and pairs that rugged before any feed saw them are missing, so the measured rug rate is a floor.',
29
+ 'Narrative, followed-wallet, social and dev-sell signals are not in the recorded history, so those triggers and exits never fire in a backtest.',
30
+ 'Mechanical entries (no agent): a lockstep backtest has no agent latency and no agent judgement; agent backtests cost Claude usage and stay small-n.',
31
+ ];
32
+ /** A closed trade at or below this is counted as a rug (the challenge's rugAtPct). */
33
+ export const RUG_PCT = -80;
34
+ export async function backtest(strategy, tape, sessions = null) {
35
+ // task-1645: a strategy in mode real backtests as its paper twin (a backtest never emits a trade-fire signal).
36
+ const run = await runAutotrade({ strategy: strategy.mode === 'real' ? { ...strategy, mode: 'paper' } : strategy, tape, sessions, pacing: { mode: 'lockstep' } });
37
+ return { report: report(strategy, tape, run), run };
38
+ }
39
+ export function report(s, tape, run) {
40
+ // Positions still open at the end are closed at their liquidation mark (0 when stuck) for the totals.
41
+ const recovered = (fills) => fills.some((f) => f.reason === 'cost-recovery');
42
+ const closed = run.trades.map((t) => ({ pnl: t.pnlUsd, pct: t.pnlPct, exit: t.exitReason, profile: t.profileId, trig: t.entryTrigger, stuck: t.stuck, cost: t.costUsd, recovered: recovered(t.fills) }));
43
+ for (const p of run.openAtEnd) {
44
+ const pos = run.book.open.get(p.mint);
45
+ const pnl = p.markUsd + pos.realizedUsd - pos.costUsd;
46
+ closed.push({ pnl, pct: round((pnl / pos.costUsd) * 100), exit: p.stuck ? 'end-stuck' : 'end-of-tape', profile: pos.profileId, trig: [], stuck: p.stuck, cost: pos.costUsd, recovered: recovered(pos.fills) });
47
+ }
48
+ const best = closed.length ? closed.reduce((a, t) => (t.pnl > a.pnl ? t : a)) : null;
49
+ const wins = closed.filter((t) => t.pnl > 0);
50
+ const losses = closed.filter((t) => t.pnl <= 0);
51
+ const net = closed.reduce((a, t) => a + t.pnl, 0);
52
+ const gw = wins.reduce((a, t) => a + t.pnl, 0);
53
+ const gl = -losses.reduce((a, t) => a + t.pnl, 0);
54
+ const bucket = (key) => {
55
+ const out = {};
56
+ for (const t of closed)
57
+ for (const k of key(t)) {
58
+ const b = (out[k] ??= { n: 0, pnlUsd: 0 });
59
+ b.n++;
60
+ b.pnlUsd = round(b.pnlUsd + t.pnl);
61
+ }
62
+ return out;
63
+ };
64
+ const avg = (xs) => (xs.length ? round(xs.reduce((a, b) => a + b, 0) / xs.length) : null);
65
+ const w = wilson(wins.length, closed.length);
66
+ const first = tape.ticks[0]?.t, last = tape.ticks[tape.ticks.length - 1]?.t;
67
+ return {
68
+ strategy: { id: s.id, version: s.version, hash: s.hash },
69
+ tape: { source: tape.source, mints: tape.mints.length, ticks: tape.ticks.length, from: first ? new Date(first).toISOString() : null, to: last ? new Date(last).toISOString() : null },
70
+ trades: closed.length, wins: wins.length, losses: losses.length,
71
+ winRate: w ? { value: round(w.value * 100, 1), lo: round(w.lo * 100, 1), hi: round(w.hi * 100, 1) } : null,
72
+ netUsd: round(net), netPct: round((net / s.bankroll.startUsd) * 100),
73
+ profitFactor: gl > 0 ? round(gw / gl) : null,
74
+ avgWinPct: avg(wins.map((t) => t.pct)), avgLossPct: avg(losses.map((t) => t.pct)),
75
+ worstPct: closed.length ? Math.min(...closed.map((t) => t.pct)) : null, bestPct: closed.length ? Math.max(...closed.map((t) => t.pct)) : null,
76
+ maxDrawdownPct: run.maxDrawdownPct,
77
+ tail: {
78
+ rugs: closed.filter((t) => t.pct <= RUG_PCT || t.stuck).length, costRecovered: closed.filter((t) => t.recovered).length, netWithoutBestUsd: round(net - (best?.pnl ?? 0)), bestMultiple: best ? round((best.pnl + best.cost) / best.cost) : null,
79
+ perEntry: bootstrapMean(closed.map((t) => (t.pnl / t.cost) * 100)),
80
+ multiples: closed.length ? (() => { const m = closed.map((t) => (t.pnl + t.cost) / t.cost).sort((a, b) => a - b); return { p50: round(quantile(m, 0.5)), p90: round(quantile(m, 0.9)), max: round(m[m.length - 1]) }; })() : null,
81
+ },
82
+ stuck: closed.filter((t) => t.stuck).length, openAtEnd: run.openAtEnd.length,
83
+ candidates: run.candidates, sessions: run.sessions, sessionCostUsd: round(run.sessionCostUsd, 4),
84
+ byExit: bucket((t) => [t.exit]), byProfile: bucket((t) => [t.profile]), byTrigger: bucket((t) => (t.trig.length ? t.trig : ['(open at end)'])),
85
+ clamps: run.clamps, refusals: run.refusals,
86
+ limits: DATA_LIMITS,
87
+ };
88
+ }
89
+ export function renderReport(r) {
90
+ const usd = (x) => `${x >= 0 ? '+' : '-'}$${Math.abs(x).toFixed(2)}`;
91
+ const rows = (o) => Object.entries(o).sort((a, b) => b[1].n - a[1].n).map(([k, b]) => ` ${k.padEnd(22)} ${String(b.n).padStart(4)} ${usd(b.pnlUsd)}`).join('\n');
92
+ return [
93
+ `BACKTEST ${r.strategy.id} v${r.strategy.version} (${r.strategy.hash}) · PAPER · recorded data`,
94
+ `tape: ${r.tape.mints} coins, ${r.tape.ticks} rows, ${r.tape.from ?? '—'} → ${r.tape.to ?? '—'}`,
95
+ `trades ${r.trades} · wins ${r.wins} · losses ${r.losses} · win rate ${r.winRate ? `${r.winRate.value}% (95% ${r.winRate.lo}–${r.winRate.hi}%)` : '—'}`,
96
+ `net ${usd(r.netUsd)} (${r.netPct}% of the bankroll) · profit factor ${r.profitFactor ?? '—'} · avg win ${r.avgWinPct ?? '—'}% · avg loss ${r.avgLossPct ?? '—'}% · best ${r.bestPct ?? '—'}% · worst ${r.worstPct ?? '—'}%`,
97
+ `tail: rugs (<= ${RUG_PCT}% or stuck) ${r.tail.rugs} · cost taken back ${r.tail.costRecovered} · net without the best trade ${usd(r.tail.netWithoutBestUsd)} · best trade ${r.tail.bestMultiple ?? '—'}x its cost`,
98
+ ` per entry ${r.tail.perEntry ? `${r.tail.perEntry.meanPct}% of the stake (bootstrap 95% ${r.tail.perEntry.lo}% to ${r.tail.perEntry.hi}%)` : '—'} · exit multiple P50 ${r.tail.multiples?.p50 ?? '—'}x · P90 ${r.tail.multiples?.p90 ?? '—'}x · max ${r.tail.multiples?.max ?? '—'}x`,
99
+ `max drawdown ${r.maxDrawdownPct}% · stuck (unsellable) ${r.stuck} · open at end ${r.openAtEnd} · candidates ${r.candidates} · sessions ${r.sessions} ($${r.sessionCostUsd})`,
100
+ ' by exit rule:', rows(r.byExit),
101
+ ' by profile:', rows(r.byProfile),
102
+ ' by entry trigger:', rows(r.byTrigger),
103
+ ` clamps fired: ${Object.entries(r.clamps).map(([k, n]) => `${k} ×${n}`).join(', ') || 'none'}`,
104
+ ` entries refused: ${Object.entries(r.refusals).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, n]) => `${k} ×${n}`).join(', ') || 'none'}`,
105
+ r.trades < SKILL_MIN_N ? ` NOTE: ${r.trades} trades is below n = ${SKILL_MIN_N}: read the counts, not the rates.` : '',
106
+ ' limits of this data:', ...r.limits.map((l) => ` - ${l}`),
107
+ ].filter(Boolean).join('\n');
108
+ }
109
+ // ───────────────────────────── held-out split + sweep ─────────────────────────────
110
+ /** Chronological split by each coin's first row: the newest `testFrac` of coins is TEST. */
111
+ export function splitTape(tape, testFrac = 0.3) {
112
+ const first = new Map();
113
+ for (const k of tape.ticks)
114
+ if (!first.has(k.mint))
115
+ first.set(k.mint, k.t);
116
+ const order = [...first.entries()].sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
117
+ const nTest = Math.max(1, Math.round(order.length * testFrac));
118
+ const testSet = new Set(order.slice(order.length - nTest).map(([m]) => m));
119
+ const cutoff = order[order.length - nTest]?.[1] ?? 0;
120
+ const pick = (inTest) => ({
121
+ source: `${tape.source} [${inTest ? 'test' : 'train'}]`,
122
+ ticks: tape.ticks.filter((k) => testSet.has(k.mint) === inTest),
123
+ mints: tape.mints.filter((m) => testSet.has(m) === inTest),
124
+ });
125
+ return { train: pick(false), test: pick(true), cutoff };
126
+ }
127
+ export function withParams(s, params) {
128
+ const doc = JSON.parse(JSON.stringify({ ...s, hash: undefined, exits: { default: s.exits.default, profiles: Object.values(s.exits.profiles) } }));
129
+ delete doc.hash;
130
+ for (const [path, v] of Object.entries(params)) {
131
+ const parts = path.split('.');
132
+ let o = doc;
133
+ if (parts[0] === 'profile') {
134
+ o = doc.exits.profiles.find((p) => p.id === s.exits.default);
135
+ parts.shift();
136
+ }
137
+ for (const p of parts.slice(0, -1)) {
138
+ const next = o[p];
139
+ if (next === null || typeof next !== 'object')
140
+ throw new Error(`sweep path ${path}: ${p} is not an object`);
141
+ o = next;
142
+ }
143
+ o[parts[parts.length - 1]] = v;
144
+ }
145
+ const r = validateStrategy(doc);
146
+ if (!r.ok)
147
+ throw new Error(`sweep params ${JSON.stringify(params)} make the strategy invalid: ${r.errors.map((e) => `${e.path} ${e.msg}`).join('; ')}`);
148
+ return r.strategy;
149
+ }
150
+ const pickR = (r) => ({ trades: r.trades, netUsd: r.netUsd, maxDrawdownPct: r.maxDrawdownPct, winRate: r.winRate });
151
+ /** Seeded PRNG (mulberry32) so a random sweep is reproducible. */
152
+ export function rng(seed) {
153
+ let a = seed >>> 0;
154
+ return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
155
+ }
156
+ export function combos(grid, mode, max, seed = 1) {
157
+ const keys = Object.keys(grid);
158
+ let all = [{}];
159
+ for (const k of keys)
160
+ all = all.flatMap((c) => grid[k].map((v) => ({ ...c, [k]: v })));
161
+ if (mode === 'grid')
162
+ return all.slice(0, max);
163
+ const r = rng(seed);
164
+ const out = [];
165
+ const pool = [...all];
166
+ while (out.length < max && pool.length)
167
+ out.push(pool.splice(Math.floor(r() * pool.length), 1)[0]);
168
+ return out;
169
+ }
170
+ export async function sweep(s, tape, grid, o = {}) {
171
+ const { train, test, cutoff } = splitTape(tape, o.testFrac ?? 0.3);
172
+ const rows = [];
173
+ const invalid = [];
174
+ for (const params of combos(grid, o.mode ?? 'grid', o.max ?? 64, o.seed ?? 1)) {
175
+ let st;
176
+ // A combination the validator refuses (e.g. TP1 above TP2) is skipped and counted, never run.
177
+ try {
178
+ st = withParams(s, params);
179
+ }
180
+ catch (e) {
181
+ invalid.push({ params, error: e.message.replace(/^.*invalid: /, '') });
182
+ continue;
183
+ }
184
+ rows.push({ params, train: pickR((await backtest(st, train)).report) });
185
+ }
186
+ if (!rows.length)
187
+ throw new Error('every sweep combination is invalid');
188
+ const minN = o.minTrainTrades ?? 5;
189
+ const eligible = rows.filter((r) => r.train.trades >= minN);
190
+ const best = [...(eligible.length ? eligible : rows)].sort((a, b) => b.train.netUsd - a.train.netUsd || a.train.maxDrawdownPct - b.train.maxDrawdownPct)[0];
191
+ // TEST is touched exactly twice: the unchanged strategy and the train winner.
192
+ const baseline = { params: {}, train: pickR((await backtest(s, train)).report), test: pickR((await backtest(s, test)).report) };
193
+ best.test = pickR((await backtest(withParams(s, best.params), test)).report);
194
+ const verdict = sweepVerdict(best, baseline);
195
+ return {
196
+ split: { trainMints: train.mints.length, testMints: test.mints.length, cutoff: new Date(cutoff).toISOString() },
197
+ combos: rows.length, invalid, baseline, best, top: [...rows].sort((a, b2) => b2.train.netUsd - a.train.netUsd).slice(0, 5), verdict,
198
+ };
199
+ }
200
+ /**
201
+ * Verdict rule 6 (§3a): gain wording only when TRAIN and TEST both have >= SKILL_MIN_N trades, the held-out net
202
+ * is above 0, the winner beats the unchanged strategy on TEST, and train agrees. A loser that loses less is
203
+ * reported as exactly that, never as a gain.
204
+ */
205
+ export function sweepVerdict(best, baseline) {
206
+ const t = best.test, b = baseline.test;
207
+ if (best.train.trades < SKILL_MIN_N || t.trades < SKILL_MIN_N)
208
+ return `too few trades (train ${best.train.trades}, held-out ${t.trades}; both need >= ${SKILL_MIN_N}): no tuning claim`;
209
+ if (t.netUsd <= 0) {
210
+ return t.netUsd > b.netUsd
211
+ ? `no edge: the tuned strategy still loses on the held-out coins (${t.netUsd.toFixed(2)} USD; it only loses ${(t.netUsd - b.netUsd).toFixed(2)} USD less than unchanged): no tuning claim`
212
+ : `no edge: the held-out net is ${t.netUsd.toFixed(2)} USD (unchanged ${b.netUsd.toFixed(2)}): keep the strategy as is`;
213
+ }
214
+ if (t.netUsd > b.netUsd && best.train.netUsd > baseline.train.netUsd)
215
+ return `held-out gain ${(t.netUsd - b.netUsd).toFixed(2)} USD over the unchanged strategy, held-out net ${t.netUsd.toFixed(2)} USD (train and test agree)`;
216
+ return `no held-out gain (test ${t.netUsd.toFixed(2)} vs unchanged ${b.netUsd.toFixed(2)}): keep the strategy as is`;
217
+ }
218
+ export function renderSweep(r) {
219
+ const usd = (x) => `${x >= 0 ? '+' : '-'}$${Math.abs(x).toFixed(2)}`;
220
+ const row = (x) => `${JSON.stringify(x.params)} train ${x.train.trades} trades ${usd(x.train.netUsd)} dd ${x.train.maxDrawdownPct}%${x.test ? ` · held-out ${x.test.trades} trades ${usd(x.test.netUsd)} dd ${x.test.maxDrawdownPct}%` : ''}`;
221
+ return [
222
+ `SWEEP · PAPER · recorded data · split by first sighting: train ${r.split.trainMints} coins, held-out ${r.split.testMints} coins (newest, from ${r.split.cutoff})`,
223
+ `combinations run ${r.combos} · refused as invalid ${r.invalid.length}`,
224
+ ` unchanged: ${row(r.baseline)}`,
225
+ ` train best: ${row(r.best)}`,
226
+ ' top 5 on train:', ...r.top.map((x) => ` ${row(x)}`),
227
+ `VERDICT: ${r.verdict}`,
228
+ ' The held-out coins were touched twice (the unchanged strategy and the train winner); nothing was tuned on them.',
229
+ ].join('\n');
230
+ }
231
+ // ───────────────────────────── tail statistics ─────────────────────────────
232
+ export function quantile(sorted, q) {
233
+ if (!sorted.length)
234
+ return NaN;
235
+ const i = (sorted.length - 1) * q;
236
+ const lo = Math.floor(i), hi = Math.ceil(i);
237
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (i - lo);
238
+ }
239
+ /** Mean with a seeded percentile-bootstrap 95% interval (deterministic: same trades → same interval). */
240
+ export function bootstrapMean(xs, resamples = 2000, seed = 1) {
241
+ if (!xs.length)
242
+ return null;
243
+ const mean = xs.reduce((a, b) => a + b, 0) / xs.length;
244
+ const r = rng(seed);
245
+ const means = [];
246
+ for (let b = 0; b < resamples; b++) {
247
+ let sum = 0;
248
+ for (let i = 0; i < xs.length; i++)
249
+ sum += xs[Math.floor(r() * xs.length)];
250
+ means.push(sum / xs.length);
251
+ }
252
+ means.sort((a, b) => a - b);
253
+ return { meanPct: round(mean), lo: round(quantile(means, 0.025)), hi: round(quantile(means, 0.975)) };
254
+ }
255
+ // ───────────────────────────── every-candidate evaluation copy ─────────────────────────────
256
+ /**
257
+ * The every-candidate copy (§3a run b): each candidate is entered at a fixed unit size with the book caps lifted
258
+ * (exposure, concurrency, day count, day-loss halt, drawdown size-down and halt, loss-streak cooldown), on a
259
+ * book large enough that cash never binds. The strategy's own entry rules, per-coin rules, exit profiles and the
260
+ * fill model are unchanged, so every entry is an independent sample and the result does not depend on which
261
+ * candidates happened to arrive first. An evaluation copy, never a strategy to run.
262
+ */
263
+ export function everyCandidate(s, unitUsd = 20) {
264
+ const doc = JSON.parse(JSON.stringify({ ...s, hash: undefined, exits: { default: s.exits.default, profiles: Object.values(s.exits.profiles) } }));
265
+ doc.id = `${s.id.slice(0, 21)}-every`;
266
+ doc.name = 'Every candidate (evaluation copy)';
267
+ doc.description = `Evaluation copy of ${s.id}: every candidate entered at a fixed $${unitUsd} with the book caps lifted, so each entry is an independent sample.`;
268
+ doc.bankroll = { ...doc.bankroll, startUsd: 100_000 };
269
+ doc.sizing = { ...doc.sizing, mode: 'fixed', fixedUsd: unitUsd, maxUsd: unitUsd, minUsd: Math.min(doc.sizing.minUsd, unitUsd), ladder: undefined };
270
+ delete doc.sizing.pct;
271
+ delete doc.sizing.riskPct;
272
+ delete doc.sizing.confidence;
273
+ delete doc.sizing.ladder;
274
+ const r = doc.risk;
275
+ doc.risk = {
276
+ ...r,
277
+ dailyLossHaltPct: 100,
278
+ drawdown: { sizeDownAtPct: 99, sizeDownMult: 1, recoverBelowPct: 0, haltAtPct: 100 },
279
+ maxConcurrent: 20, maxExposurePct: 100, maxEntriesPerDay: 500,
280
+ lossStreak: { losses: 50, cooldownMin: 0 },
281
+ ...(r.recoveredBags ? { recoveredBags: { ...r.recoveredBags, maxBags: 50 } } : {}),
282
+ };
283
+ const v = validateStrategy(doc);
284
+ if (!v.ok)
285
+ throw new Error(`every-candidate copy of ${s.id} is invalid: ${v.errors.map((e) => `${e.path} ${e.msg}`).join('; ')}`);
286
+ return v.strategy;
287
+ }
288
+ /**
289
+ * Runner recall (§3a item 3). A coin is in the window while its age is inside the strategy's entry age range and
290
+ * its liquidity is at least the default profile's fit.minLiquidityUsd. Its peak is the best price at that
291
+ * liquidity from the first window row on, divided by the first window row's price. For each threshold: how many
292
+ * such coins ran >= Nx, how many the entry rules passed on some window row, which rule refused the others (a coin
293
+ * counts once per rule) and which exit closed each one the run entered.
294
+ */
295
+ export function runnerRecall(s, tape, run, thresholds) {
296
+ const rules = new EntryRules(s);
297
+ const minLiq = s.exits.profiles[s.exits.default].fit.minLiquidityUsd;
298
+ const { min, max } = s.entry.ageHours;
299
+ const byMint = new Map();
300
+ for (const k of tape.ticks) {
301
+ const a = byMint.get(k.mint);
302
+ if (a)
303
+ a.push(k);
304
+ else
305
+ byMint.set(k.mint, [k]);
306
+ }
307
+ const trades = new Map();
308
+ for (const t of run.trades)
309
+ if (!trades.has(t.mint))
310
+ trades.set(t.mint, { exit: t.exitReason, pnlPct: t.pnlPct, sells: t.fills.slice(1).map((f) => `${f.reason} ${(f.price / t.fills[0].price).toFixed(2)}x`) });
311
+ for (const p of run.openAtEnd)
312
+ if (!trades.has(p.mint))
313
+ trades.set(p.mint, { exit: p.stuck ? 'end-stuck' : 'end-of-tape', pnlPct: null, sells: [] });
314
+ const coins = [];
315
+ for (const [mint, ks] of byMint) {
316
+ const win = ks.filter((k) => k.kind === 'live' && k.createdAt !== null && k.price && (k.liq ?? 0) >= minLiq && (k.t - k.createdAt) / 3600e3 >= min && (max === null || (k.t - k.createdAt) / 3600e3 <= max));
317
+ if (!win.length)
318
+ continue;
319
+ const p0 = win[0].price;
320
+ const peak = Math.max(...ks.filter((k) => k.t >= win[0].t && (k.liq ?? 0) >= minLiq && k.price).map((k) => k.price));
321
+ const res = win.map((k) => rules.screen(k));
322
+ const refusedBy = {};
323
+ for (const r of res)
324
+ if (!r.pass)
325
+ for (const f of r.failed)
326
+ refusedBy[f] = (refusedBy[f] ?? 0) + 1;
327
+ coins.push({ mint, symbol: ks[0].symbol, peakX: round(peak / p0, 1), windowRows: win.length, passed: res.some((r) => r.pass), refusedBy, trade: trades.get(mint) ?? null });
328
+ }
329
+ return thresholds.map((x) => {
330
+ const rs = coins.filter((c) => c.peakX >= x).sort((a, b) => b.peakX - a.peakX);
331
+ const refusedBy = {};
332
+ for (const c of rs)
333
+ if (!c.passed)
334
+ for (const f of Object.keys(c.refusedBy))
335
+ refusedBy[f] = (refusedBy[f] ?? 0) + 1;
336
+ const exits = {};
337
+ for (const c of rs)
338
+ if (c.trade)
339
+ exits[c.trade.exit] = (exits[c.trade.exit] ?? 0) + 1;
340
+ return { minX: x, runners: rs.length, passed: rs.filter((c) => c.passed).length, entered: rs.filter((c) => c.trade).length, closedGreen: rs.filter((c) => (c.trade?.pnlPct ?? 0) > 0).length, refusedBy, exits, coins: rs };
341
+ });
342
+ }
343
+ export function renderRecall(rs, s) {
344
+ const L = [];
345
+ const top = (o) => Object.entries(o).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ${n}`).join(', ') || 'none';
346
+ for (const r of rs) {
347
+ L.push(`RUNNER RECALL >= ${r.minX}x (${s.id}: coins in its entry window that later ran >= ${r.minX}x at sellable liquidity): ${r.runners} · entry rules passed ${r.passed} · entered ${r.entered} · closed green ${r.closedGreen}`);
348
+ L.push(` refused runners by rule (a coin counts once per rule): ${top(r.refusedBy)}`);
349
+ L.push(` how the entered runners closed: ${top(r.exits)}`);
350
+ for (const c of r.coins.slice(0, 25)) {
351
+ const short = `${c.mint.slice(0, 4)}…${c.mint.slice(-4)}`;
352
+ const why = c.passed ? 'PASSED ' : `refused ${Object.entries(c.refusedBy).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([f, n]) => `${f} ${n}/${c.windowRows}`).join(', ')}`;
353
+ const tr = c.trade ? ` · ${c.trade.sells.join(' → ') || '(no sell)'} · ${c.trade.pnlPct === null ? 'open at end' : `closed ${c.trade.pnlPct}% by ${c.trade.exit}`}` : '';
354
+ L.push(` ${short} ${String(c.peakX).padStart(6)}x window rows ${String(c.windowRows).padStart(3)} ${why}${tr}`);
355
+ }
356
+ }
357
+ return L.join('\n');
358
+ }
@@ -0,0 +1,32 @@
1
+ // task-1537: the price basis in autotrade text (the runner's lines, the agents' DOSSIER / ENGINE notes, the ledger's
2
+ // fill JSON). Market cap by default, from the tick's own cap and price (the supply that source counts) or pump.fun's
3
+ // fixed 1B; no supply → the price and why. Display and wording only: the engine, exits and caps read prices.
4
+ import { DEFAULT_PRICE_BASIS, fmtCap, fmtUsdPrice, fmtX, knownSupply, withMcap } from '../../data/src/basis.js';
5
+ export { DEFAULT_PRICE_BASIS };
6
+ /** The supply hints one tick carries (the resolved cap when the task-1518 split rule picked one). */
7
+ export function tickHints(k) {
8
+ if (!k)
9
+ return {};
10
+ return { mint: k.mint, launchpad: k.launchpad, price: k.price, mcap: k.mcapResolved ?? k.mcap, via: 'tick' };
11
+ }
12
+ /** A level in the basis: `$69.7K mcap` / a raw price (price basis, unchanged) / `0.0000697 (price; supply unknown)`. */
13
+ export function levelText(basis, price, k) {
14
+ if (price === null || price === undefined || !Number.isFinite(price))
15
+ return '—';
16
+ if (basis !== 'mcap')
17
+ return String(price);
18
+ const sup = knownSupply(tickHints(k));
19
+ return sup ? `${fmtCap(price * sup.tokens)} mcap` : `${fmtUsdPrice(price)} (price; supply unknown)`;
20
+ }
21
+ /** A row of levels (history, ticks) in the basis: caps at the current supply, else raw prices. */
22
+ export function levelsText(basis, prices, k) {
23
+ const sup = basis === 'mcap' ? knownSupply(tickHints(k)) : null;
24
+ return prices.map((p) => (p === null || !Number.isFinite(p) ? '—' : sup ? fmtCap(p * sup.tokens) : String(p))).join(' ');
25
+ }
26
+ /** `x` multiple of the current level over the entry (same in either basis). */
27
+ export function multipleText(entry, now) { return fmtX(entry, now) ?? '—'; }
28
+ /** The ledger's fill JSON keeps BOTH: `price` / `avgPrice` and their caps `mcap` / `avgMcap` (null without a supply). */
29
+ export function fillMcaps(view, k) {
30
+ const h = tickHints(k);
31
+ return withMcap(view, { price: 'mcap', avgPrice: 'avgMcap' }, { mint: h.mint, launchpad: h.launchpad, mcap: h.mcap, price: h.price });
32
+ }