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
@@ -18,11 +18,22 @@
18
18
  // (c) EVERY CALL IS VALIDATED, JOURNALED AND HALTABLE. Before any state change the desk re-reads the control
19
19
  // surface (pause / take-over / stop) and the panic file and re-checks the mandate is alive; a refusal is
20
20
  // journaled too, so the spectator view and the audit log see it. The user always wins.
21
+ //
22
+ // task-1506 (AUTOTRADE AT-4, additive): the autotrade position monitor sells through `PaperExitDesk`, a subclass
23
+ // at the end of this file, so the MCP desk's own surface is unchanged.
24
+ // P1 desk-side EXIT PLANS: an entry registers its exit plan (`agent.paper.exit.plan`), and every monitor sell
25
+ // is an `exit()` journaled as `agent.paper.exit.<rule>` (cost-recovery, bag-trail, tp1, stop, …) before
26
+ // its fill; a plan is done (`agent.paper.exit.done`) once the position is empty.
27
+ // P2 a `close-only` control state: sells (exits) still run, even on an expired or revoked mandate, while every
28
+ // buy is refused. A panic still halts everything.
29
+ // P4 a `note` on fills (who, which plan, the exit rule, the agent's own reason), journaled with the fill.
30
+ // None of these raises a cap or adds a buy path: a note and a plan are journal data, an exit is a sell.
21
31
  import { systemClock } from "../../clock.js";
22
32
  import { mandateAlive, TIERS } from "../plans/mandate.js";
23
33
  import { KillSwitch } from "../kill-switch.js";
24
34
  import { capsHash } from "../plans/mandate.js";
25
35
  import { SOL_MINT, buildQuoteRequest, lamportsToSol, solToLamports, rawToTokens, } from "../../paper.js";
36
+ import { buildFillTag, buildLateTag } from "../ledger/tags.js";
26
37
  /** The confirmation tier an agent is pinned to: the weakest, so an agent can never match a human keypress. */
27
38
  export const AGENT_TIER = 'hands-free';
28
39
  /** A refusal from the desk. `reason` is a stable code; `message` is human text (both are journaled). */
@@ -45,7 +56,40 @@ export function isRateLimitedQuoteError(e) {
45
56
  return /jupiter/.test(e.message);
46
57
  return e.name === 'HttpError' && e.status === 429;
47
58
  }
59
+ /** task-1530: most `tag` calls per minute (a journal-only call, but still bounded). */
60
+ export const TAG_RATE_PER_MIN = 30;
61
+ const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9:_.-]{0,79}$/;
48
62
  const isFiniteNum = (x) => typeof x === 'number' && Number.isFinite(x);
63
+ const NOTE_KEY = /^[A-Za-z][A-Za-z0-9_.-]{0,39}$/;
64
+ export function sanitizeFillNote(note) {
65
+ if (typeof note === 'string') {
66
+ const s = note.slice(0, 400);
67
+ return s ? s : undefined;
68
+ }
69
+ if (!note || typeof note !== 'object' || Array.isArray(note))
70
+ return undefined;
71
+ const out = {};
72
+ let n = 0;
73
+ for (const [k, v] of Object.entries(note)) {
74
+ if (n >= 16)
75
+ break;
76
+ if (!NOTE_KEY.test(k))
77
+ continue;
78
+ if (typeof v === 'string')
79
+ out[k] = v.slice(0, 200);
80
+ else if (isFiniteNum(v) || typeof v === 'boolean' || v === null)
81
+ out[k] = v;
82
+ else
83
+ continue;
84
+ n++;
85
+ }
86
+ return n ? out : undefined;
87
+ }
88
+ /** Exit rule → journal slug (`time_review:no-answer` → `time-review-no-answer`). `plan` and `done` are reserved. */
89
+ export function exitRuleSlug(rule) {
90
+ const s = String(rule).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
91
+ return s && s !== 'plan' && s !== 'done' ? s : null;
92
+ }
49
93
  /**
50
94
  * The paper agent desk. One instance per armed mandate + paper wallet. All four verbs (open / size / confirm /
51
95
  * close) and both reads (positions / pnl) run here; nothing else is exposed, and there is no cap setter.
@@ -65,6 +109,8 @@ export class PaperAgentDesk {
65
109
  agent;
66
110
  quoteRetry;
67
111
  onQuoteRetry;
112
+ sessionSrc;
113
+ tagTimes = [];
68
114
  tickets = new Map();
69
115
  ticketSeq = 0;
70
116
  constructor(deps) {
@@ -87,6 +133,7 @@ export class PaperAgentDesk {
87
133
  const r = deps.quoteRetry ?? DEFAULT_QUOTE_RETRY;
88
134
  this.quoteRetry = { maxAttempts: Math.max(1, Math.floor(r.maxAttempts)), maxWaitMs: Math.max(0, r.maxWaitMs), maxTotalMs: Math.max(0, r.maxTotalMs) };
89
135
  this.onQuoteRetry = deps.onQuoteRetry ?? null;
136
+ this.sessionSrc = deps.sessionId ?? (() => null);
90
137
  }
91
138
  /** The agent's effective per-trade cap: the mandate cap times the weakest-tier multiplier. */
92
139
  perTradeCapUsd() {
@@ -108,15 +155,18 @@ export class PaperAgentDesk {
108
155
  * The gate every mutating call runs first: panic file → control surface → mandate alive. A failure here is
109
156
  * journaled and throws, so a paused / taken-over / stopped session (or a dead mandate) halts the agent.
110
157
  */
111
- gate(stage, extra = {}) {
158
+ gate(stage, extra = {}, side) {
112
159
  const panic = this.kill.engaged();
113
160
  if (panic)
114
161
  this.refuse(stage, 'panic', `trading is halted (panic): ${panic.reason}`, extra);
115
162
  const c = this.control();
116
- if (c.state !== 'running') {
163
+ // close-only (task-1506 P2): a sell passes the control surface and a dead mandate; a buy never does.
164
+ const exitOnly = c.state === 'close-only' && side === 'sell';
165
+ if (c.state !== 'running' && !exitOnly) {
117
166
  const msg = c.state === 'takeover' ? 'the user has taken over — agent paper trading is paused'
118
167
  : c.state === 'paused' ? `agent paper trading is paused${c.reason ? `: ${c.reason}` : ''}`
119
- : `agent paper trading is stopped${c.reason ? `: ${c.reason}` : ''}`;
168
+ : c.state === 'close-only' ? `agent paper trading is close-only${c.reason ? `: ${c.reason}` : ''}; exits run, new buys are refused`
169
+ : `agent paper trading is stopped${c.reason ? `: ${c.reason}` : ''}`;
120
170
  this.refuse(stage, c.state, msg, extra);
121
171
  }
122
172
  const m = this.mandateSrc();
@@ -124,7 +174,7 @@ export class PaperAgentDesk {
124
174
  this.refuse(stage, 'no-mandate', 'no armed mandate is available for agent paper trading', extra);
125
175
  this.mandate = m;
126
176
  const alive = mandateAlive(this.mandate, this.clock.now());
127
- if (!alive.ok)
177
+ if (!alive.ok && !exitOnly)
128
178
  this.refuse(stage, alive.reason, `the mandate is not usable (${alive.reason})`, extra);
129
179
  }
130
180
  requireAction(stage, side, extra = {}) {
@@ -188,7 +238,7 @@ export class PaperAgentDesk {
188
238
  /** Open an inert PAPER ticket for a mint + side. Validates the gate, the action and the allow-list only. */
189
239
  open(input) {
190
240
  const side = input.side;
191
- this.gate('open', { mint: input.mint, side });
241
+ this.gate('open', { mint: input.mint, side }, side);
192
242
  if (typeof input.mint !== 'string' || input.mint.length < 32)
193
243
  this.refuse('open', 'bad-request', 'a base58 mint is required');
194
244
  if (!isFiniteNum(input.decimals) || input.decimals < 0 || input.decimals > 18)
@@ -208,7 +258,7 @@ export class PaperAgentDesk {
208
258
  const t = this.tickets.get(ticketId);
209
259
  if (!t)
210
260
  this.refuse('size', 'unknown-ticket', 'no such open ticket');
211
- this.gate('size', { ticketId });
261
+ this.gate('size', { ticketId }, t.side);
212
262
  if (t.side === 'buy') {
213
263
  const usd = input.usd;
214
264
  if (!isFiniteNum(usd) || usd <= 0)
@@ -229,21 +279,22 @@ export class PaperAgentDesk {
229
279
  return { ...t };
230
280
  }
231
281
  /** Confirm a sized ticket: re-check the gate and the caps, fetch a FRESH quote, book the PAPER fill. */
232
- async confirm(ticketId) {
282
+ async confirm(ticketId, opts = {}) {
233
283
  const t = this.tickets.get(ticketId);
234
284
  if (!t)
235
285
  this.refuse('confirm', 'unknown-ticket', 'no such open ticket');
236
- this.gate('confirm', { ticketId });
286
+ this.gate('confirm', { ticketId }, t.side);
237
287
  this.requireAction('confirm', t.side, { ticketId });
238
288
  this.requireMintAllowed('confirm', t.mint, { ticketId });
239
289
  const order = await this.buildOrder('confirm', t);
240
- const { rec, quoteAttempts } = await this.book('confirm', order, { ticketId });
290
+ const tag = buildFillTag(t.side, opts.tag);
291
+ const { rec, quoteAttempts } = await this.book('confirm', order, { ticketId }, sanitizeFillNote(opts.note), tag);
241
292
  this.tickets.delete(ticketId);
242
- return { fill: rec, position: this.positionView(this.wallet.position(t.mint)), quoteAttempts };
293
+ return { fill: rec, position: this.positionView(this.wallet.position(t.mint)), quoteAttempts, tag };
243
294
  }
244
295
  /** Close a fraction of a held PAPER position in one call (open sell → size fraction → confirm). */
245
296
  async close(input) {
246
- this.gate('close', { mint: input.mint });
297
+ this.gate('close', { mint: input.mint }, 'sell');
247
298
  this.requireAction('close', 'sell', { mint: input.mint });
248
299
  this.requireMintAllowed('close', input.mint, {});
249
300
  const pos = this.wallet.position(input.mint);
@@ -255,8 +306,9 @@ export class PaperAgentDesk {
255
306
  const t = { id: `pt_close_${++this.ticketSeq}`, mint: input.mint, symbol: pos.symbol, decimals: pos.decimals, side: 'sell', usd: null, fraction, openedAt: this.clock.now() };
256
307
  this.note('agent.paper.close', { mint: input.mint, fraction });
257
308
  const order = await this.buildOrder('close', t);
258
- const { rec, quoteAttempts } = await this.book('close', order, { mint: input.mint });
259
- return { fill: rec, position: this.positionView(this.wallet.position(input.mint)), quoteAttempts };
309
+ const tag = buildFillTag('sell', input.tag);
310
+ const { rec, quoteAttempts } = await this.book('close', order, { mint: input.mint }, sanitizeFillNote(input.note), tag);
311
+ return { fill: rec, position: this.positionView(this.wallet.position(input.mint)), quoteAttempts, tag };
260
312
  }
261
313
  /** Build the paper order from a ticket, re-checking the buy caps against the live wallet at execute time. */
262
314
  async buildOrder(stage, t) {
@@ -331,11 +383,11 @@ export class PaperAgentDesk {
331
383
  }
332
384
  catch { /* the spectator is best-effort */ }
333
385
  await this.clock.sleep(wait);
334
- this.gate(stage, extra);
386
+ this.gate(stage, extra, order.side);
335
387
  }
336
388
  }
337
389
  /** Fetch a fresh quote and book the fill through the PAPER executor; journal the booked fill. */
338
- async book(stage, order, extra) {
390
+ async book(stage, order, extra, note, tag) {
339
391
  const { quote, attempts } = await this.freshQuote(stage, order, extra);
340
392
  if (attempts > 1)
341
393
  extra = { ...extra, quoteAttempts: attempts };
@@ -345,7 +397,9 @@ export class PaperAgentDesk {
345
397
  this.refuse(stage, 'price-impact', `price impact ${quote.priceImpactPct.toFixed(2)}% exceeds the mandate max ${this.mandate.limits.maxPriceImpactPct}%`, extra);
346
398
  }
347
399
  // The control surface may have flipped while the quote was in flight: re-check right before booking.
348
- this.gate(stage, extra);
400
+ this.gate(stage, extra, order.side);
401
+ // task-1530: the book (cash + open cost) just before this fill, for the trade ledger's % of book
402
+ const bookLamports = this.bookLamports();
349
403
  let rec;
350
404
  try {
351
405
  rec = await this.executor.execute(order, quote);
@@ -357,9 +411,63 @@ export class PaperAgentDesk {
357
411
  ...extra, mint: rec.mint, symbol: rec.symbol, side: rec.side, paper: true,
358
412
  solDelta: rec.solDelta.toString(), tokenDelta: rec.tokenDelta.toString(),
359
413
  priceSol: rec.priceSol, priceUsd: rec.priceUsd, realizedLamports: rec.realizedLamports.toString(), fillId: rec.id,
414
+ ...(note !== undefined ? { note } : {}),
415
+ // task-1530: the facts the unified trade ledger needs, journaled with the fill (older journals lack them; the
416
+ // ledger then joins them from the paper wallet's own table)
417
+ decimals: rec.decimals, feeLamports: rec.feeLamports.toString(), impactPct: rec.priceImpactPct, slippageBps: rec.slippageBps,
418
+ ...(bookLamports !== null ? { bookLamports: bookLamports.toString() } : {}),
419
+ ...(tag ? { tag } : {}),
420
+ ...(this.sessionId() ? { sessionId: this.sessionId() } : {}),
360
421
  });
361
422
  return { rec: rec, quoteAttempts: attempts };
362
423
  }
424
+ /** The model session id the launcher named, if it is a plain id. */
425
+ sessionId() {
426
+ let s = null;
427
+ try {
428
+ s = this.sessionSrc();
429
+ }
430
+ catch {
431
+ s = null;
432
+ }
433
+ return typeof s === 'string' && SESSION_ID.test(s) ? s : null;
434
+ }
435
+ /** Paper cash + the cost basis of every open position, lamports (null when the wallet cannot be read). */
436
+ bookLamports() {
437
+ try {
438
+ return this.wallet.balance() + this.wallet.positions().reduce((a, p) => a + (p.tokensRaw > 0n ? p.costLamports : 0n), 0n);
439
+ }
440
+ catch {
441
+ return null;
442
+ }
443
+ }
444
+ /**
445
+ * task-1530: declare a PROFILE, REPROFILE or EXIT for a coin this desk traded, apart from its fill (a REPROFILE,
446
+ * or a line that was not passed with the fill: journaled `agent.paper.tag`, which the trade ledger reads as a
447
+ * late declaration). Journal-only: it books nothing, moves no money and works while trading is paused or stopped
448
+ * (a reason can always be recorded). The coin must be one the paper wallet holds or has held.
449
+ */
450
+ tag(input) {
451
+ const now = this.clock.now();
452
+ this.tagTimes = this.tagTimes.filter((t) => now - t < 60_000);
453
+ if (this.tagTimes.length >= TAG_RATE_PER_MIN)
454
+ throw new PaperDeskError('rate-limited', `at most ${TAG_RATE_PER_MIN} tags a minute`);
455
+ if (typeof input.mint !== 'string' || !this.wallet.position(input.mint))
456
+ throw new PaperDeskError('no-position', 'tag a coin the paper wallet holds or has traded (pass its mint)');
457
+ const g = buildLateTag(input);
458
+ if (!g.ok)
459
+ throw new PaperDeskError('bad-request', g.why);
460
+ if (g.tagKind === 'reprofile') {
461
+ const pos = this.wallet.position(input.mint);
462
+ if (!pos || pos.tokensRaw <= 0n)
463
+ throw new PaperDeskError('no-position', 'a REPROFILE needs a position that is still held');
464
+ }
465
+ this.tagTimes.push(now);
466
+ const declared = g.tagKind === 'profile' ? { profile: g.profile, reason: g.reason }
467
+ : g.tagKind === 'reprofile' ? { from: g.from, to: g.to, reason: g.reason } : { exitReason: g.exitReason, reason: g.reason };
468
+ this.note('agent.paper.tag', { mint: input.mint, tagKind: g.tagKind, ...declared, ...(g.line ? { line: g.line } : {}), ...(this.sessionId() ? { sessionId: this.sessionId() } : {}) });
469
+ return { recorded: true, mint: input.mint, tagKind: g.tagKind, declared };
470
+ }
363
471
  // ───────────────────────────── the two reads ─────────────────────────────
364
472
  positionView(pos) {
365
473
  if (!pos)
@@ -398,4 +506,108 @@ export class PaperAgentDesk {
398
506
  };
399
507
  }
400
508
  }
509
+ /**
510
+ * task-1506 P1: the paper desk the autotrade monitor sells through. It is the same desk (every gate, cap, quote,
511
+ * journal and the paper executor), plus exit plans and rule-named exits. It is a separate class on purpose: the
512
+ * MCP paper tools hold a plain PaperAgentDesk, whose surface stays exactly the six reviewed verbs.
513
+ */
514
+ export class PaperExitDesk extends PaperAgentDesk {
515
+ /** Exit plans by mint (one held position per mint in the paper wallet). */
516
+ plans = new Map();
517
+ async confirm(ticketId, opts = {}) {
518
+ const r = await super.confirm(ticketId, opts);
519
+ if (r.fill.side === 'sell')
520
+ this.planDoneIfEmpty(r.fill.mint, 'confirm');
521
+ return r;
522
+ }
523
+ async close(input) {
524
+ const r = await super.close(input);
525
+ this.planDoneIfEmpty(input.mint, 'close');
526
+ return r;
527
+ }
528
+ /**
529
+ * Register the exit plan for a held position (P1). Journal data only: it needs a held paper position and moves
530
+ * no money, so it runs in every control state (a restart re-registers its plans, journaled `restored: true`).
531
+ * Registering the same position again returns the existing plan.
532
+ */
533
+ registerExitPlan(input) {
534
+ const pos = this.wallet.position(input.mint);
535
+ if (!pos || pos.tokensRaw <= 0n)
536
+ this.refuse('exit-plan', 'no-position', 'no open paper position in this mint to plan exits for', { mint: input.mint });
537
+ const ok = (v, n) => typeof v === 'string' && v.length > 0 && v.length <= n;
538
+ if (!ok(input.owner, 40) || !ok(input.positionId, 80) || !ok(input.profileId, 64))
539
+ this.refuse('exit-plan', 'bad-request', 'an exit plan needs an owner, a position id and a profile id', { mint: input.mint });
540
+ const have = this.plans.get(input.mint);
541
+ if (have && have.positionId === input.positionId)
542
+ return { ...have };
543
+ const note = sanitizeFillNote(input.note);
544
+ const plan = {
545
+ planId: `xp_${++this.ticketSeq}_${this.clock.now().toString(36)}`, mint: input.mint, symbol: pos.symbol, owner: input.owner,
546
+ positionId: input.positionId, profileId: input.profileId, runId: input.runId ?? null, registeredAt: this.clock.now(), ...(note ? { note } : {}),
547
+ };
548
+ this.plans.set(input.mint, plan);
549
+ this.note('agent.paper.exit.plan', { ...plan, ...(input.restored === true ? { restored: true } : {}) });
550
+ return { ...plan };
551
+ }
552
+ /** The registered exit plans (read-only copies). */
553
+ exitPlans() { return [...this.plans.values()].map((p) => ({ ...p })); }
554
+ /**
555
+ * Sell a fraction of a planned position as one exit rule (P1): journaled `agent.paper.exit.<rule>` before the
556
+ * fill, the fill carries the note, and the plan is done once the position is empty. Runs in `close-only`.
557
+ */
558
+ async exit(input) {
559
+ const rule = exitRuleSlug(input.rule);
560
+ this.gate('exit', { mint: input.mint }, 'sell');
561
+ if (!rule)
562
+ this.refuse('exit', 'bad-request', 'an exit needs a rule name', { mint: input.mint });
563
+ const plan = this.plans.get(input.mint);
564
+ if (!plan)
565
+ this.refuse('exit', 'no-exit-plan', 'no exit plan is registered for this mint', { mint: input.mint, rule });
566
+ this.requireAction('exit', 'sell', { mint: input.mint });
567
+ this.requireMintAllowed('exit', input.mint, {});
568
+ const pos = this.wallet.position(input.mint);
569
+ if (!pos || pos.tokensRaw <= 0n)
570
+ this.refuse('exit', 'no-position', 'no open paper position in this mint to exit', { mint: input.mint, rule });
571
+ const fraction = input.fraction ?? 1;
572
+ if (!isFiniteNum(fraction) || !(fraction > 0 && fraction <= 1))
573
+ this.refuse('exit', 'bad-request', 'fraction must be in (0, 1]', { mint: input.mint, rule });
574
+ const t = { id: `pt_exit_${++this.ticketSeq}`, mint: input.mint, symbol: pos.symbol, decimals: pos.decimals, side: 'sell', usd: null, fraction, openedAt: this.clock.now() };
575
+ this.note(`agent.paper.exit.${rule}`, { planId: plan.planId, positionId: plan.positionId, mint: input.mint, fraction, ...(input.detail ? { detail: String(input.detail).slice(0, 200) } : {}) });
576
+ const order = await this.buildOrder('exit', t);
577
+ const base = sanitizeFillNote(input.note);
578
+ const note = { ...(typeof base === 'object' ? base : base ? { text: base } : {}), exit: rule, planId: plan.planId, positionId: plan.positionId };
579
+ const { rec, quoteAttempts } = await this.book('exit', order, { mint: input.mint, rule }, sanitizeFillNote(note));
580
+ this.planDoneIfEmpty(input.mint, rule);
581
+ return { fill: rec, position: this.positionView(this.wallet.position(input.mint)), quoteAttempts, plan: { ...plan }, rule: rule };
582
+ }
583
+ /**
584
+ * A read-only sell QUOTE for a fraction of a held position (what an exit would net, after the simulated fee),
585
+ * for sizing a cost-recovery slice (task-1506). It books nothing and journals only a refusal.
586
+ */
587
+ async previewSell(input) {
588
+ this.gate('preview', { mint: input.mint }, 'sell');
589
+ const pos = this.wallet.position(input.mint);
590
+ if (!pos || pos.tokensRaw <= 0n)
591
+ this.refuse('preview', 'no-position', 'no open paper position in this mint', { mint: input.mint });
592
+ const fraction = input.fraction;
593
+ if (!isFiniteNum(fraction) || !(fraction > 0 && fraction <= 1))
594
+ this.refuse('preview', 'bad-request', 'fraction must be in (0, 1]', { mint: input.mint });
595
+ const t = { id: `pt_preview_${++this.ticketSeq}`, mint: input.mint, symbol: pos.symbol, decimals: pos.decimals, side: 'sell', usd: null, fraction, openedAt: this.clock.now() };
596
+ const order = await this.buildOrder('preview', t);
597
+ const { quote } = await this.freshQuote('preview', order, { mint: input.mint });
598
+ const f = this.executor.previewFill(order, quote);
599
+ return { amountRaw: order.amountRaw, solOut: lamportsToSol(f.solDelta > 0n ? f.solDelta : 0n), priceImpactPct: f.priceImpactPct };
600
+ }
601
+ /** Once a planned position is empty its plan is done (journaled once). */
602
+ planDoneIfEmpty(mint, by) {
603
+ const plan = this.plans.get(mint);
604
+ if (!plan)
605
+ return;
606
+ const pos = this.wallet.position(mint);
607
+ if (pos && pos.tokensRaw > 0n)
608
+ return;
609
+ this.plans.delete(mint);
610
+ this.note('agent.paper.exit.done', { planId: plan.planId, positionId: plan.positionId, mint, by });
611
+ }
612
+ }
401
613
  export { SOL_MINT };
@@ -54,8 +54,9 @@ export function readPaperAgentRendezvous(env = process.env) {
54
54
  return null;
55
55
  }
56
56
  }
57
- const VALID_STATES = ['running', 'paused', 'stopped', 'takeover'];
58
- /** Write the live control state (0600). The app calls this on pause / take-over / stop / resume. */
57
+ const VALID_STATES = ['running', 'paused', 'stopped', 'takeover', 'close-only'];
58
+ /** Write the live control state (0600). The app calls this on pause / take-over / stop / resume, and autotrade's
59
+ * soft stop writes `close-only` (task-1506 P2: exits keep running, buys are refused). */
59
60
  export function writeDeskControl(control, env = process.env) {
60
61
  const p = paperAgentControlPath(env);
61
62
  mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
@@ -7,9 +7,7 @@
7
7
  // Security posture (mirrors research/popup-host/server/server.mjs §5):
8
8
  // * listens on 127.0.0.1 only; never 0.0.0.0 / ::. task-1368 (g): on a FIXED preferred port
9
9
  // (`WALLET_BRIDGE_PORT`, random only when it is busy) — Phantom remembers trust PER ORIGIN, and a
10
- // random port made every page a brand-new origin: the sign page then called signTransaction on an
11
- // origin Phantom had never connected and got 4100 ("not authorized"). The sign page now connects
12
- // first and checks the account; the stable origin lets that connect resolve silently next time.
10
+ // random port made every page a brand-new origin, so Phantom asked to connect every time.
13
11
  // * a single-use, random 32-byte TOKEN in the URL; a second use is refused (replay-proof).
14
12
  // * strict Host check on every request; strict Origin + JSON content-type + 16 KB body cap on POST.
15
13
  // * remote address must be loopback; anything else gets an empty 404.
@@ -23,32 +21,13 @@
23
21
  import http from 'node:http';
24
22
  import { randomBytes, timingSafeEqual } from 'node:crypto';
25
23
  import { spawn } from 'node:child_process';
26
- import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
24
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
27
25
  import { tmpdir } from 'node:os';
28
26
  import { join } from 'node:path';
29
27
  import { systemClock } from "../clock.js";
30
28
  import { base58 } from "../sources/solana-rpc.js";
31
29
  import { verifyEd25519 } from "./tx-decode.js";
32
30
  const TOKEN_TTL_MS = 120_000;
33
- /**
34
- * task-1328: the tx-sign page needs @solana/web3.js in the BROWSER (Phantom's signTransaction takes a
35
- * VersionedTransaction object). The vendored IIFE bundle (packages/data/vendor/solana-web3, MIT,
36
- * sha256-pinned by the tests) is inlined into the page under the CSP nonce — no CDN, no remote script.
37
- * Read lazily and cached; a missing file leaves the page in its "needs the shell" state (closed).
38
- */
39
- export const SOLANA_WEB3_VENDOR = new URL('../../vendor/solana-web3/index.iife.min.js', import.meta.url);
40
- let web3Bundle;
41
- export function solanaWeb3Bundle() {
42
- if (web3Bundle !== undefined)
43
- return web3Bundle;
44
- try {
45
- web3Bundle = readFileSync(SOLANA_WEB3_VENDOR, 'utf8');
46
- }
47
- catch {
48
- web3Bundle = null;
49
- }
50
- return web3Bundle;
51
- }
52
31
  const MAX_BODY = 16 * 1024;
53
32
  const LOOPBACK = new Set(['127.0.0.1', '::ffff:127.0.0.1', '::1']);
54
33
  function spawnOpener(target) {
@@ -93,7 +72,7 @@ export const systemBrowserOpener = (url) => {
93
72
  };
94
73
  /**
95
74
  * The wallet bridge's preferred loopback port: `http://127.0.0.1:41291` is the ORIGIN Phantom stores
96
- * its trust for (link and sign share it). Unassigned by IANA; the X login callback is 41287.
75
+ * its trust for. Unassigned by IANA; the X login callback is 41287.
97
76
  */
98
77
  export const WALLET_BRIDGE_PORT = 41291;
99
78
  function b64url(buf) { return buf.toString('base64url'); }
@@ -223,7 +202,7 @@ function oneShot(args) {
223
202
  res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
224
203
  res.end(JSON.stringify({ ok: out.error === undefined, error: out.error ?? null }));
225
204
  // Resolve (and close every connection) only once the page has its answer: `finish` now tears
226
- // the socket down, and the page's "✓ signed" line reads this response.
205
+ // the socket down, and the page's "✓ linked" line reads this response.
227
206
  if (out.error === undefined) {
228
207
  const done = () => finish(() => resolve(out.value));
229
208
  res.once('finish', done);
@@ -350,38 +329,6 @@ export class WalletBridge {
350
329
  },
351
330
  });
352
331
  }
353
- /**
354
- * Hand an unsigned transaction to the wallet for signing and resolve with the signed bytes. dexterm
355
- * never holds a key; the browser approval IS the physical confirmation (SPEC §6.5). The unsigned tx
356
- * is the plan the user already confirmed; a popup-host lane can swap the page for a chromeless window.
357
- */
358
- signTransaction(args) {
359
- const started = this.clock.now();
360
- return oneShot({
361
- route: 'sign',
362
- open: this.open,
363
- timeoutMs: this.timeoutMs,
364
- clock: this.clock,
365
- preferredPort: this.port,
366
- page: ({ port, nonce: cspNonce, portNote }) => signPage(port, cspNonce, args.unsignedTxBase64, args.expectPublicKey ?? '', args.label ?? 'swap', portNote),
367
- onResult: (body) => {
368
- // task-1368 (g): the page reports which account signed (base58). When the caller named the
369
- // linked wallet, a different account is refused HERE too — the page's own check is UX, this is
370
- // the gate (the signer also verifies the signature over the bytes; this is belt and braces).
371
- const account = str(body.publicKey);
372
- if (args.expectPublicKey && account !== '' && account !== args.expectPublicKey)
373
- return { error: `signed by a different wallet account (${sanitize(account, 8)}…), not the linked wallet` };
374
- const signed = str(body.signedTx);
375
- if (signed.length === 0)
376
- return { error: 'wallet returned no signed transaction' };
377
- if (signed.length > 4096)
378
- return { error: 'signed transaction too large' };
379
- if (!/^[A-Za-z0-9+/=]+$/.test(signed))
380
- return { error: 'signed transaction is not base64' };
381
- return { value: { signedTxBase64: signed, provider: sanitize(body.provider ?? 'unknown', 24), roundTripMs: this.clock.now() - started } };
382
- },
383
- });
384
- }
385
332
  }
386
333
  // --------------------------------------------------------------------------- inlined browser pages
387
334
  // These run in the user's browser (real Phantom). The fake-Phantom e2e POSTs the result directly, so
@@ -417,48 +364,3 @@ const j=await rp.json();if(j.ok){$('r').textContent='✓ linked. Return to dexte
417
364
  catch(e){$('r').textContent='✗ '+(e&&e.message||e);$('r').className='bad';$('s').textContent='';$('go').disabled=false;}}
418
365
  $('go').addEventListener('click',run);poll(0);})();</script></body></html>`.replace('"__T__"', `new URLSearchParams(location.search).get('t')`);
419
366
  }
420
- function signPage(port, nonce, unsignedTxB64, expectPk, label, portNote = '') {
421
- const web3 = solanaWeb3Bundle();
422
- return `<!doctype html><html lang="en"><head><meta charset="utf-8">
423
- <meta name="viewport" content="width=device-width, initial-scale=1"><title>dexterm · sign</title>
424
- <style nonce="${nonce}">body{font:14px/1.5 -apple-system,system-ui,sans-serif;background:#07090d;color:#d7dbe0;margin:0;padding:2rem}
425
- .card{max-width:640px;margin:auto;background:#0d1016;border:1px solid #1d2430;border-radius:12px;padding:1.5rem}
426
- h1{font-size:1.1rem;color:#7fd1ff}button{font:inherit;background:#531;border:1px solid #a62;color:#fec;border-radius:8px;padding:.5rem 1rem;cursor:pointer}
427
- .ok{color:#5de28a}.bad{color:#ff6b6b}.warn{color:#e2b85d}.muted{color:#7a828e}code{word-break:break-all}</style></head><body><div class="card">
428
- <h1>✦ dexterm · approve ${esc(label)}</h1>
429
- <p>Your terminal already showed you this trade and you pressed confirm. Approve it in your wallet to sign. dexterm submits the signed transaction; it never holds your key.</p>
430
- ${portNote ? `<p class="warn">${esc(portNote)}</p>\n` : ''}<p id="p" class="warn">looking for a wallet…</p>
431
- <p><button id="go" disabled>Approve in wallet</button> <span id="s"></span></p>
432
- <p id="r"></p>
433
- <p class="muted">linked wallet: <code>${esc(expectPk || 'any')}</code> · connected: <code id="a">–</code></p></div>
434
- ${web3 ? `<script nonce="${nonce}">${web3}\n;window.dexSolana=solanaWeb3;</script>` : ''}
435
- <script nonce="${nonce}">(()=>{'use strict';const $=i=>document.getElementById(i),PORT=${port},EXPECT=${JSON.stringify(expectPk)};
436
- const TX=${JSON.stringify(unsignedTxB64)};const b64=u=>btoa(String.fromCharCode.apply(null,u));
437
- const short=k=>k&&k.length>12?k.slice(0,4)+'…'+k.slice(-4):String(k||'?');
438
- // task-1368 (g): Phantom's error codes in plain English. 4100 = this origin was never connected (or the
439
- // user revoked it): the fix is the Connect prompt, not a retry of the signature.
440
- function explain(e){const code=e&&(e.code!==undefined?e.code:(e.error&&e.error.code));const msg=(e&&e.message)||String(e);
441
- if(code===4100)return 'Phantom has not authorized this page (4100). Press Approve again and accept the Connect prompt; if none appears, open Phantom → Settings → Trusted apps, allow 127.0.0.1:'+PORT+', then confirm the trade again in dexterm. Nothing was signed.';
442
- if(code===4001)return 'you rejected the request in Phantom; nothing was signed. Confirm again in dexterm to retry.';
443
- if(code===-32603)return 'Phantom could not process the transaction (internal error): '+msg;
444
- return msg;}
445
- function bytes(b){const s=atob(b),a=new Uint8Array(s.length);for(let i=0;i<s.length;i++)a[i]=s.charCodeAt(i);return a;}
446
- let prov=null,name='none';function find(){if(window.phantom&&window.phantom.solana&&window.phantom.solana.isPhantom)return[window.phantom.solana,'phantom'];if(window.solana&&window.solana.isPhantom)return[window.solana,'phantom'];if(window.solana)return[window.solana,window.solana.isStubWallet?'stub':'solana'];return[null,'none'];}
447
- function poll(n){[prov,name]=find();if(prov){$('p').textContent='wallet: '+name;$('p').className='ok';$('go').disabled=false;if(name==='stub')run();return;}if(n>40){$('p').textContent='No Solana wallet found.';$('p').className='bad';return;}setTimeout(()=>poll(n+1),100);}
448
- async function run(){$('go').disabled=true;$('s').textContent='connecting to your wallet…';try{
449
- // Real Phantom needs @solana/web3.js on the page to rebuild the VersionedTransaction before signTransaction().
450
- // The dexterm wallet shell (popup-host lane) provides it as window.dexSolana; see scripts/ for the manual path.
451
- if(!window.dexSolana||!window.dexSolana.VersionedTransaction)throw new Error('tx signing needs the dexterm wallet shell (window.dexSolana). See scripts/phantom-devnet.md.');
452
- // task-1368 (g): CONNECT FIRST. signTransaction() on an origin Phantom has not connected is a 4100, so
453
- // the page used to fail on every fresh (random-port) origin. connect() resolves silently on a trusted
454
- // origin and prompts once on a new one; then the connected account must be the wallet dexterm linked.
455
- const res=await prov.connect();const pk=(res&&res.publicKey)||prov.publicKey;const account=pk?(typeof pk.toBase58==='function'?pk.toBase58():String(pk)):'';
456
- if(!account)throw new Error('the wallet connected without a public key; nothing was signed');$('a').textContent=account;
457
- if(EXPECT&&account!==EXPECT)throw new Error('Phantom is on account '+short(account)+' but dexterm linked '+short(EXPECT)+'. Switch accounts in Phantom and press Approve again, or re-link with "dexterm wallet link". Nothing was signed.');
458
- $('s').textContent='approve in your wallet…';
459
- const vt=window.dexSolana.VersionedTransaction.deserialize(bytes(TX));const signed=await prov.signTransaction(vt);
460
- const out=b64(signed.serialize());const rp=await fetch('http://127.0.0.1:'+PORT+'/sign/result',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({t:${JSON.stringify('__T__')},signedTx:out,provider:name,publicKey:account})});
461
- const j=await rp.json();if(j.ok){$('r').textContent='✓ signed. Return to dexterm.';$('r').className='ok';$('s').textContent='done';}else{$('r').textContent='✗ '+(j.error||'rejected');$('r').className='bad';$('go').disabled=false;}}
462
- catch(e){$('r').textContent='✗ '+explain(e);$('r').className='bad';$('s').textContent='';$('go').disabled=false;}}
463
- $('go').addEventListener('click',run);poll(0);})();</script></body></html>`.replace('"__T__"', `new URLSearchParams(location.search).get('t')`);
464
- }
@@ -0,0 +1,101 @@
1
+ // task-1530 — ledger EXPORTS: one row per trade (the folded TradeRow), in three formats.
2
+ //
3
+ // csv RFC 4180, a header row, one line per trade; a cell that starts with = + - @ is prefixed with '
4
+ // so a spreadsheet never runs it as a formula (CSV injection), and every text cell is one line.
5
+ // json {schema, exportedAt, unit, ledgerHead, trades: TradeRow[]} with the fills and the reprofiles.
6
+ // parquet-lite a COLUMNAR JSON document (not Apache Parquet; no dependency): {schema, columns: [{name, type}],
7
+ // rows, data: {column: values[]}}. pandas: pd.DataFrame(doc["data"]); polars: pl.DataFrame(doc["data"]).
8
+ //
9
+ // No export holds a key or a balance beyond what the trades themselves imply.
10
+ export const EXPORT_FORMATS = ['csv', 'json', 'parquet-lite'];
11
+ export const EXPORT_SCHEMA = 'dexterm-ledger-export/1';
12
+ export const PARQUET_LITE_SCHEMA = 'dexterm-parquet-lite/1';
13
+ const iso = (ms) => (ms === null ? null : new Date(ms).toISOString());
14
+ /** The flat columns, in order. Shared by csv and parquet-lite so the two never disagree. */
15
+ export const EXPORT_COLUMNS = [
16
+ { name: 'trade_id', type: 'string', get: (t) => t.tradeId },
17
+ { name: 'source', type: 'string', get: (t) => t.source },
18
+ { name: 'wallet_mode', type: 'string', get: (t) => t.walletMode },
19
+ { name: 'status', type: 'string', get: (t) => t.status },
20
+ { name: 'mint', type: 'string', get: (t) => t.mint },
21
+ { name: 'symbol', type: 'string', get: (t) => t.symbol },
22
+ { name: 'strategy_id', type: 'string', get: (t) => t.strategy?.id ?? null },
23
+ { name: 'strategy_version', type: 'string', get: (t) => t.strategy?.version ?? null },
24
+ { name: 'strategy_hash', type: 'string', get: (t) => t.strategy?.hash ?? null },
25
+ { name: 'run_id', type: 'string', get: (t) => t.runId },
26
+ { name: 'account', type: 'string', get: (t) => t.account ?? null },
27
+ { name: 'profile', type: 'string', get: (t) => t.profile.id },
28
+ { name: 'profile_source', type: 'string', get: (t) => t.profile.source },
29
+ { name: 'reprofiles', type: 'number', get: (t) => t.reprofiles.length },
30
+ { name: 'entry_reason', type: 'string', get: (t) => t.entryReason.text },
31
+ { name: 'entry_reason_source', type: 'string', get: (t) => t.entryReason.source },
32
+ { name: 'exit_reason', type: 'string', get: (t) => t.exitReason.code },
33
+ { name: 'exit_reason_text', type: 'string', get: (t) => t.exitReason.text },
34
+ { name: 'exit_reason_source', type: 'string', get: (t) => t.exitReason.source },
35
+ { name: 'opened_at', type: 'timestamp', get: (t) => iso(t.openedAt) },
36
+ { name: 'closed_at', type: 'timestamp', get: (t) => iso(t.closedAt) },
37
+ { name: 'hold_sec', type: 'number', get: (t) => t.holdSec },
38
+ { name: 'buys', type: 'number', get: (t) => t.buys },
39
+ { name: 'sells', type: 'number', get: (t) => t.sells },
40
+ { name: 'size_sol', type: 'number', get: (t) => t.sizeSol },
41
+ { name: 'size_usd', type: 'number', get: (t) => t.sizeUsd },
42
+ { name: 'pct_of_book', type: 'number', get: (t) => t.pctOfBook },
43
+ { name: 'entry_price_sol', type: 'number', get: (t) => t.entryPriceSol },
44
+ { name: 'entry_price_usd', type: 'number', get: (t) => t.entryPriceUsd },
45
+ { name: 'entry_mcap_usd', type: 'number', get: (t) => t.entryMcapUsd },
46
+ { name: 'exit_price_sol', type: 'number', get: (t) => t.exitPriceSol },
47
+ { name: 'exit_price_usd', type: 'number', get: (t) => t.exitPriceUsd },
48
+ { name: 'exit_mcap_usd', type: 'number', get: (t) => t.exitMcapUsd },
49
+ { name: 'supply_tokens', type: 'number', get: (t) => t.supply?.tokens ?? null },
50
+ { name: 'supply_source', type: 'string', get: (t) => t.supply?.source ?? null },
51
+ { name: 'proceeds_sol', type: 'number', get: (t) => t.proceedsSol },
52
+ { name: 'proceeds_usd', type: 'number', get: (t) => t.proceedsUsd },
53
+ { name: 'pnl_sol', type: 'number', get: (t) => t.pnlSol },
54
+ { name: 'pnl_usd', type: 'number', get: (t) => t.pnlUsd },
55
+ { name: 'pnl_pct', type: 'number', get: (t) => t.pnlPct },
56
+ { name: 'fees_sol', type: 'number', get: (t) => t.feesSol },
57
+ { name: 'impact_pct_entry', type: 'number', get: (t) => t.impactPctEntry },
58
+ { name: 'impact_pct_exit', type: 'number', get: (t) => t.impactPctExit },
59
+ { name: 'slippage_bps', type: 'number', get: (t) => t.slippageBps },
60
+ { name: 'mfe_pct', type: 'number', get: (t) => t.mfePct },
61
+ { name: 'mae_pct', type: 'number', get: (t) => t.maePct },
62
+ { name: 'excursion_basis', type: 'string', get: (t) => t.excursionBasis },
63
+ { name: 'rug', type: 'boolean', get: (t) => t.rug },
64
+ { name: 'rug_why', type: 'string', get: (t) => t.rugWhy },
65
+ { name: 'stuck', type: 'boolean', get: (t) => t.stuck },
66
+ { name: 'tx_sigs', type: 'string', get: (t) => (t.txSigs.length ? t.txSigs.join(' ') : null) },
67
+ { name: 'session_ids', type: 'string', get: (t) => (t.model.sessionIds.length ? t.model.sessionIds.join(' ') : null) },
68
+ { name: 'models', type: 'string', get: (t) => (t.model.models.length ? t.model.models.join(' ') : null) },
69
+ { name: 'model_cost_usd', type: 'number', get: (t) => t.model.costUsd },
70
+ ];
71
+ /** One CSV cell: quoted when needed, one line, never a formula. */
72
+ export function csvCell(v) {
73
+ if (v === null)
74
+ return '';
75
+ if (typeof v !== 'string')
76
+ return String(v);
77
+ let s = v.replace(/[\r\n\t]+/g, ' ');
78
+ if (/^[=+\-@]/.test(s))
79
+ s = `'${s}`;
80
+ return /[",]/.test(s) || s !== s.trim() ? `"${s.replace(/"/g, '""')}"` : s;
81
+ }
82
+ export function toCsv(rows) {
83
+ const head = EXPORT_COLUMNS.map((c) => c.name).join(',');
84
+ return `${[head, ...rows.map((t) => EXPORT_COLUMNS.map((c) => csvCell(c.get(t))).join(','))].join('\n')}\n`;
85
+ }
86
+ export function toJson(rows, m) {
87
+ return `${JSON.stringify({ schema: EXPORT_SCHEMA, exportedAt: iso(m.exportedAt), unit: m.unit, ledgerHead: m.ledgerHead, filters: m.filters ?? {}, count: rows.length, trades: rows }, null, 2)}\n`;
88
+ }
89
+ export function toParquetLite(rows, m) {
90
+ const data = {};
91
+ for (const c of EXPORT_COLUMNS)
92
+ data[c.name] = rows.map((t) => c.get(t));
93
+ return `${JSON.stringify({ schema: PARQUET_LITE_SCHEMA, exportedAt: iso(m.exportedAt), ledgerHead: m.ledgerHead, unit: m.unit, rows: rows.length, columns: EXPORT_COLUMNS.map((c) => ({ name: c.name, type: c.type })), data })}\n`;
94
+ }
95
+ export function exportTrades(rows, format, m) {
96
+ if (format === 'csv')
97
+ return toCsv(rows);
98
+ if (format === 'json')
99
+ return toJson(rows, m);
100
+ return toParquetLite(rows, m);
101
+ }