dexterm-cli 0.1.7 → 0.2.1

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 (233) hide show
  1. package/dist/packages/agents/src/spectator.js +31 -3
  2. package/dist/packages/agents/src/tiers.js +12 -0
  3. package/dist/packages/agents/src/tools.js +13 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +7 -2
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +5 -2
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +21 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  30. package/dist/packages/autotrade/src/rugproof.js +231 -0
  31. package/dist/packages/autotrade/src/runner.js +194 -28
  32. package/dist/packages/autotrade/src/service-guard.js +125 -0
  33. package/dist/packages/autotrade/src/service-install.js +215 -0
  34. package/dist/packages/autotrade/src/service-recover.js +232 -0
  35. package/dist/packages/autotrade/src/service.js +289 -0
  36. package/dist/packages/autotrade/src/sessions.js +16 -7
  37. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  38. package/dist/packages/autotrade/src/signal.js +196 -0
  39. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  40. package/dist/packages/autotrade/src/strategy.js +15 -2
  41. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  42. package/dist/packages/classifier/src/calibrate.js +138 -0
  43. package/dist/packages/classifier/src/classify.js +107 -0
  44. package/dist/packages/classifier/src/config.js +220 -0
  45. package/dist/packages/classifier/src/dossier.js +95 -0
  46. package/dist/packages/classifier/src/hardware.js +63 -0
  47. package/dist/packages/classifier/src/index.js +18 -0
  48. package/dist/packages/classifier/src/models.js +111 -0
  49. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  50. package/dist/packages/classifier/src/providers/http.js +63 -0
  51. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  52. package/dist/packages/classifier/src/providers/llm.js +89 -0
  53. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  54. package/dist/packages/classifier/src/questions.js +97 -0
  55. package/dist/packages/classifier/src/registry.js +42 -0
  56. package/dist/packages/classifier/src/snapshot.js +31 -0
  57. package/dist/packages/classifier/src/systemone.js +88 -0
  58. package/dist/packages/classifier/src/types.js +18 -0
  59. package/dist/packages/cli/assets/dash/dash.css +139 -0
  60. package/dist/packages/cli/assets/dash/dash.js +200 -0
  61. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  62. package/dist/packages/cli/assets/dash/index.html +36 -0
  63. package/dist/packages/cli/src/analyst/backend.js +3 -2
  64. package/dist/packages/cli/src/analyst/codex.js +3 -2
  65. package/dist/packages/cli/src/app-help-gen.js +26 -2
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  68. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
  70. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  71. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  72. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  73. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  74. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  75. package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
  76. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  77. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  78. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  79. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  80. package/dist/packages/cli/src/classifier-live.js +81 -0
  81. package/dist/packages/cli/src/dash/data.js +217 -0
  82. package/dist/packages/cli/src/dash/server.js +92 -0
  83. package/dist/packages/cli/src/dash-cmd.js +113 -0
  84. package/dist/packages/cli/src/doctor.js +34 -0
  85. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  86. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  87. package/dist/packages/cli/src/live/signals.js +8 -0
  88. package/dist/packages/cli/src/main.js +98 -6
  89. package/dist/packages/cli/src/mcp-follow.js +21 -1
  90. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  91. package/dist/packages/cli/src/narratives.js +114 -0
  92. package/dist/packages/cli/src/overlay/model.js +147 -0
  93. package/dist/packages/cli/src/overlay/page.js +146 -0
  94. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  95. package/dist/packages/cli/src/overlay/server.js +170 -0
  96. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  97. package/dist/packages/cli/src/pnl-card.js +4 -0
  98. package/dist/packages/cli/src/record-cmd.js +82 -11
  99. package/dist/packages/cli/src/rules-cmd.js +2 -1
  100. package/dist/packages/cli/src/rules.js +24 -4
  101. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  102. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  103. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  104. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  105. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  106. package/dist/packages/cli/src/screens/narrative.js +154 -2
  107. package/dist/packages/cli/src/state.js +1 -0
  108. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  109. package/dist/packages/cli/src/version.js +1 -1
  110. package/dist/packages/custom/src/rules.js +5 -1
  111. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  112. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  113. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  114. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  115. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  116. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  117. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  118. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  119. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  120. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  121. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  122. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  123. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  124. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  127. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  131. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  132. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  140. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  158. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  160. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  162. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  163. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  164. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  165. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  174. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  175. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  176. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  177. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  179. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  180. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  181. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  207. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  209. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  210. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  211. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  212. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  213. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  214. package/dist/packages/mcp/src/index.js +1 -1
  215. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  216. package/dist/packages/mcp/src/server.js +125 -10
  217. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  218. package/dist/packages/narratives/src/cluster.js +193 -0
  219. package/dist/packages/narratives/src/embed.js +184 -0
  220. package/dist/packages/narratives/src/export.js +60 -0
  221. package/dist/packages/narratives/src/index.js +10 -0
  222. package/dist/packages/narratives/src/label.js +48 -0
  223. package/dist/packages/narratives/src/normalize.js +179 -0
  224. package/dist/packages/narratives/src/recorder.js +93 -0
  225. package/dist/packages/narratives/src/tracker.js +328 -0
  226. package/dist/packages/recorder/src/exit-probes.js +215 -0
  227. package/dist/packages/recorder/src/index.js +1 -0
  228. package/dist/packages/recorder/src/recorder.js +11 -1
  229. package/dist/packages/recorder/src/retention.js +4 -0
  230. package/dist/packages/recorder/src/schema.js +20 -1
  231. package/dist/packages/tui/src/keymap.js +12 -0
  232. package/package.json +3 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -10,6 +10,11 @@
10
10
  // Nothing here arms a mandate, signs, holds a key or touches a wallet. task-1571: there is no `--live`: dexterm places
11
11
  // no live order. Live trading happens in the Telegram trading bot of the user's choice.
12
12
  //
13
+ // task-1645: `--real` runs a strategy armed for real mode (`dexterm autotrade signal arm`, on the user's terminal) on
14
+ // LIVE data only: the same engine as paper, and every fill first goes out as a LIVE TRADE FIRE signal (outbox +
15
+ // the channels of connectors.json `signals`, the Telegram copy through the user's bot). Never on a replay (no stale
16
+ // price may ever reach an executor), never with chat approvals (a tap never authorises a real signal).
17
+ //
13
18
  // Telegram (task-1527): when `dexterm telegram setup` stored a token and a chat is paired, the user's own bot starts
14
19
  // with the run (telegram-host.ts): notify, control through the same control file, and approve-to-trade on paper (a
15
20
  // strategy with `entry.decide: "agent+approve"`, or approvals on). --no-telegram keeps it off; an agent+approve
@@ -24,12 +29,17 @@ import { join } from 'node:path';
24
29
  import { dextermHome, loadConfig } from '../../../custom/src/index.js';
25
30
  import { pickBasis } from '../../../data/src/basis.js';
26
31
  import { KillSwitch, createDataLayer, defaultDbPath, resolveRpcUrl } from '../../../data/src/index.js';
32
+ import { DrainPoller, RugProbe, chainVets, defaultFetchJson, rugproofVet } from '../../../autotrade/src/index.js';
27
33
  import { HEARTBEAT_MS, LiveSource, badgeOf, RUNNER_RENDEZVOUS_SCHEMA, RunnerSafety, SessionGate, acquireRunLock, clearControl, clearStaleControl, controlPath, claudeSessionFactory, effectiveCaps, fileLedger, loadRunnerConfig, loadTape, newRunId, prepareAccount, releaseRunLock, renderDossierRecord, runAutotrade, runnerRendezvousPath, runnerSystemPrompt, simExecutor, stubSession, writeJsonAtomic, writeRendezvous, RUN_SUMMARY_SCHEMA, builtinTapePath, } from '../../../autotrade/src/index.js';
28
34
  import { assertAutotradeAllowed, ollamaSessionFactory } from '../../../autotrade/src/ollama-sessions.js';
35
+ import { Outbox, RealSignalExecutor, channelsFromConfig, enabledChannels, loadSignalsConfig, openRealRun, realModeCheck } from '../../../autotrade/src/index.js';
36
+ import { PaperLimitsExecutor, effectiveLimits, loadLimits } from '../../../autotrade/src/index.js';
37
+ import { digestDue, digestLines, hmacStore, limitsSummary, loadDigestState, saveDigestState, startExecutorListener } from '../../../autotrade/src/index.js';
38
+ import { digestFor } from "./report.js";
29
39
  import { parseArgs, resolveStrategyArg } from "./strategies.js";
30
40
  import { startTelegramHost } from "./telegram-host.js";
31
- const BOOL = ['paper', 'live', 'stub', 'verbose', 'quiet', 'raw', 'no-telegram'];
32
- export const RUN_USAGE = 'dexterm autotrade run <id|file> --paper [--replay TAPE [--ms-per-tick 2000]] [--engine claude|ollama] [--model M] [--max-minutes N] [--stub] [--raw] [--quiet] [--no-telegram]';
41
+ const BOOL = ['paper', 'real', 'live', 'stub', 'verbose', 'quiet', 'raw', 'no-telegram'];
42
+ export const RUN_USAGE = 'dexterm autotrade run <id|file> --paper|--real [--replay TAPE [--ms-per-tick 2000]] [--engine claude|ollama] [--model M] [--max-minutes N] [--sol-usd N] [--stub] [--raw] [--quiet] [--no-telegram]';
33
43
  /** task-1571: what `--live` says. dexterm holds no key and places no live order. */
34
44
  export const NO_LIVE = 'dexterm autotrade run: there is no --live: dexterm runs strategies on paper only and never holds a key or signs; live trading happens in the Telegram trading bot of your choice (your own bot gets the alerts: dexterm telegram setup)';
35
45
  function num(v, dflt) {
@@ -45,8 +55,12 @@ export async function runCmd(argv, io, over = {}) {
45
55
  io.stderr.write(`${NO_LIVE}\n`);
46
56
  return 2;
47
57
  }
48
- if (!opts.paper) {
49
- io.stderr.write(`dexterm autotrade run: pass --paper (autotrade runs on paper only)\nusage: ${RUN_USAGE}\n`);
58
+ if (!opts.paper && !opts.real) {
59
+ io.stderr.write(`dexterm autotrade run: pass --paper (or --real for a strategy armed for real mode)\nusage: ${RUN_USAGE}\n`);
60
+ return 2;
61
+ }
62
+ if (opts.paper && opts.real) {
63
+ io.stderr.write('dexterm autotrade run: --paper or --real, not both\n');
50
64
  return 2;
51
65
  }
52
66
  const arg = typeof opts.strategy === 'string' ? opts.strategy : pos[0];
@@ -62,11 +76,17 @@ export async function runCmd(argv, io, over = {}) {
62
76
  io.stderr.write(`error ${e.path} ${e.msg}\n`);
63
77
  return 1;
64
78
  }
65
- const strategy = r.strategy;
66
- if (strategy.mode !== 'paper') {
67
- io.stderr.write(`dexterm autotrade run: ${strategy.id} is mode "${strategy.mode}": the runner runs paper only (dexterm holds no key and places no live order)\n`);
79
+ // task-1645: --paper on a real strategy runs its paper twin (a forward test: no signal goes out).
80
+ const strategy = opts.paper && r.strategy.mode === 'real' ? { ...r.strategy, mode: 'paper' } : r.strategy;
81
+ if (opts.real && strategy.mode !== 'real') {
82
+ io.stderr.write(`dexterm autotrade run: ${strategy.id} is mode "${strategy.mode}": --real needs a strategy armed for real mode (dexterm autotrade signal arm ${strategy.id}, on your own terminal)\n`);
68
83
  return 1;
69
84
  }
85
+ if (strategy.mode !== 'paper' && strategy.mode !== 'real') {
86
+ io.stderr.write(`dexterm autotrade run: ${strategy.id} is mode "${strategy.mode}": the runner runs paper only (dexterm holds no key and places no live order); real mode, which sends signals to your own executor, is armed with dexterm autotrade signal arm\n`);
87
+ return 1;
88
+ }
89
+ const realMode = strategy.mode === 'real';
70
90
  const home = dextermHome(io.env);
71
91
  // task-1537: the run's lines and the agents' messages speak the user's basis (--price/--mcap/env, config, market cap)
72
92
  const basis = pickBasis(loadConfig(home).config.display?.priceMode, io.env);
@@ -83,6 +103,19 @@ export async function runCmd(argv, io, over = {}) {
83
103
  }
84
104
  const config = cfg.config;
85
105
  const replay = typeof opts.replay === 'string' ? opts.replay : null;
106
+ // task-1645: real mode's preconditions, before anything starts (armed at this hash, caps, kill off, a channel).
107
+ let real = null;
108
+ if (realMode) {
109
+ if (replay) {
110
+ io.stderr.write('dexterm autotrade run: --real runs on live data only (a replay would send old prices to your executor); replay its paper twin with --paper\n');
111
+ return 2;
112
+ }
113
+ real = await prepareReal(home, strategy, opts, io);
114
+ if ('error' in real) {
115
+ io.stderr.write(`dexterm autotrade run: ${real.error}\n`);
116
+ return 1;
117
+ }
118
+ }
86
119
  // task-1543: the engine for the sessions: --engine, else ai.json (surfaces.autotrade / provider); auto = claude.
87
120
  let local = null;
88
121
  if (!opts.stub) {
@@ -100,6 +133,8 @@ export async function runCmd(argv, io, over = {}) {
100
133
  const lock = acquireRunLock(home, runId);
101
134
  if (!lock.ok) {
102
135
  io.stderr.write(`dexterm autotrade run: another runner is active (pid ${lock.holder.pid}, run ${lock.holder.runId}); stop it first\n`);
136
+ if (real && !('error' in real))
137
+ real.stop();
103
138
  return 1;
104
139
  }
105
140
  if (lock.reclaimed)
@@ -109,7 +144,7 @@ export async function runCmd(argv, io, over = {}) {
109
144
  releaseRunLock(home, runId);
110
145
  return 1;
111
146
  }
112
- const badge = 'PAPER';
147
+ const badge = realMode ? 'REAL (LIVE TRADE FIRE signals to your executor)' : 'PAPER';
113
148
  // Telegram: the user's own bot, when it is set up and paired (or the host a /use switch carried over).
114
149
  let host = over.telegramHost ?? null;
115
150
  const ownHost = !over.telegramHost;
@@ -127,14 +162,103 @@ export async function runCmd(argv, io, over = {}) {
127
162
  else if (!t.quiet)
128
163
  say(`telegram: off (${t.why})`);
129
164
  }
165
+ if (realMode && host && (strategy.entry.decide === 'agent+approve' || host.service.broker.required(strategy))) {
166
+ io.stderr.write(`dexterm autotrade run: approve-to-trade over chat is paper only, so ${strategy.id} cannot take an entry in real mode (entry.decide "agent+approve" or \`dexterm telegram approvals on\`); your executor is the last gate in real mode\n`);
167
+ if (ownHost)
168
+ await host.stop();
169
+ releaseRunLock(home, runId);
170
+ return 1;
171
+ }
130
172
  if (strategy.entry.decide === 'agent+approve' && !host) {
131
173
  io.stderr.write(`dexterm autotrade run: ${strategy.id} needs a one-tap approval for every entry (entry.decide "agent+approve"), and no paired Telegram chat is connected; set it up with \`dexterm telegram setup\`\n`);
132
174
  releaseRunLock(home, runId);
133
175
  return 1;
134
176
  }
177
+ // task-1654: a paper run obeys the limits only when limits.json has a `paper` scope (a forward test that shows what
178
+ // real mode would have refused or clamped). Its state is the run's own; real mode's files are never touched.
179
+ let paperLimits = null;
180
+ if (!realMode) {
181
+ paperLimits = await preparePaperLimits(home, strategy, opts, io, !!replay);
182
+ if (paperLimits && 'error' in paperLimits) {
183
+ io.stderr.write(`dexterm autotrade run: ${paperLimits.error}\n`);
184
+ if (host && ownHost)
185
+ await host.stop();
186
+ releaseRunLock(home, runId);
187
+ return 1;
188
+ }
189
+ }
135
190
  const baseExec = simExecutor(strategy.execution);
136
191
  if (host)
137
192
  host.attach(runId, strategy, baseExec, solUsdFor);
193
+ // task-1645: the signal executor wraps the paper path (the host's port when the bot is on), so every fill is first a signal.
194
+ let realExec = null;
195
+ let outbox = null;
196
+ let pumpTimer = null;
197
+ let digestTimer = null;
198
+ let listener = null;
199
+ let bookUsd = strategy.bankroll.startUsd;
200
+ let paperExec = null;
201
+ if (paperLimits && !('error' in paperLimits)) {
202
+ paperExec = new PaperLimitsExecutor({ inner: host ? host.port : baseExec, limits: paperLimits.limits, solUsd: paperLimits.solUsd, bookUsd: () => bookUsd, onTrip: say });
203
+ }
204
+ if (real && !('error' in real)) {
205
+ const sc = real.signals;
206
+ const tg = host ? async (lines) => {
207
+ const before = host.service.audit.events.length;
208
+ await host.service.hub.broadcast({ kind: 'text', badge: 'LIVE', lines }, 'trade_fire');
209
+ if (host.service.audit.events.slice(before).some((e) => e.kind === 'out_failed'))
210
+ throw new Error('telegram send failed');
211
+ } : undefined;
212
+ const { channels, defer } = channelsFromConfig(home, sc, { telegram: tg, env: io.env });
213
+ try {
214
+ outbox = Outbox.open(home, { channels, defer });
215
+ }
216
+ catch (e) {
217
+ io.stderr.write(`dexterm autotrade run: ${e.message}\n`);
218
+ if (host && ownHost)
219
+ await host.stop();
220
+ releaseRunLock(home, runId);
221
+ return 1;
222
+ }
223
+ const swept = openRealRun(outbox, runId, strategy, real.check.arming.accountId).swept;
224
+ if (swept.length)
225
+ say(`real mode: sent "halted" (runner-lost) for ${swept.length} earlier run(s) that ended without one`);
226
+ // task-1654: every limit (the effective set for this strategy + account), and a line to the run log and the
227
+ // paired chat when the executor goes silent / comes back or a risk limit trips.
228
+ const notify = (line) => { say(line); if (host)
229
+ void host.service.hub.broadcast({ kind: 'text', badge: 'LIVE', lines: [line] }, 'real_limits').catch(() => { }); };
230
+ realExec = new RealSignalExecutor({ home, inner: host ? host.port : baseExec, outbox, strategy, arming: real.check.arming, caps: real.check.caps, limits: real.check.limits.values, notify, runId, ttlSec: sc.ttlSec, solUsd: real.solUsd, bookUsd: () => bookUsd });
231
+ pumpTimer = setInterval(() => { void outbox.pump().catch(() => { }); }, 1000);
232
+ if (sc.executor.listenPort) {
233
+ const store = hmacStore(home, sc, io.env);
234
+ try {
235
+ listener = await startExecutorListener({ home, port: sc.executor.listenPort, secret: () => { try {
236
+ return store.get();
237
+ }
238
+ catch {
239
+ return null;
240
+ } } });
241
+ say(`real mode: executor listener on 127.0.0.1:${listener.port} (POST /heartbeat, /receipt)`);
242
+ }
243
+ catch (e) {
244
+ say(`real mode: executor listener NOT started (${e.message.slice(0, 80)}); executors can still append to autotrade/real/executor.jsonl`);
245
+ }
246
+ }
247
+ }
248
+ // task-1654: the scheduled digest (opt-in: dexterm autotrade report --schedule daily|weekly), sent by a run with the bot on.
249
+ if (host) {
250
+ const tick = () => {
251
+ const st = loadDigestState(home);
252
+ const due = digestDue(st, Date.now());
253
+ if (!due)
254
+ return;
255
+ saveDigestState(home, { ...st, lastSentDay: localDayNow(), lastSentAt: Date.now() });
256
+ const d = digestFor(home, { period: due, now: Date.now() });
257
+ void host.service.hub.broadcast({ kind: 'text', badge: realMode ? 'LIVE' : 'PAPER', lines: digestLines(d) }, 'report').catch(() => { });
258
+ };
259
+ digestTimer = setInterval(tick, 10 * 60_000);
260
+ digestTimer.unref?.();
261
+ }
138
262
  const { ledger, files } = fileLedger(home, runId, host?.onLedger);
139
263
  const startedAt = Date.now();
140
264
  const gate = new SessionGate(effectiveCaps(strategy, config), config.allowanceGuard);
@@ -174,6 +298,7 @@ export async function runCmd(argv, io, over = {}) {
174
298
  let live = null;
175
299
  let dossier;
176
300
  let vet;
301
+ let drain;
177
302
  let closeData = () => { };
178
303
  if (replay) {
179
304
  try {
@@ -209,6 +334,14 @@ export async function runCmd(argv, io, over = {}) {
209
334
  const larp = strategy.entry.safety.larp;
210
335
  if (larp)
211
336
  vet = larpVet(larp, (mint) => mcp.call('larp_check', { mint }));
337
+ // task-1748: the rug-proof gates first (read-only probes), then the LARP gate; the drain guard polls held pools.
338
+ const rp = strategy.entry.safety.rugproof;
339
+ if (rp) {
340
+ const probe = new RugProbe({ fetchJson: defaultFetchJson(), rpcUrl: rpc.url });
341
+ vet = chainVets(rugproofVet(rp, { probe }), vet);
342
+ if (rp.drainGuard)
343
+ drain = new DrainPoller(rp.drainGuard, probe, (rec) => log('drain_probe', rec));
344
+ }
212
345
  }
213
346
  // Heartbeat + rendezvous: on every state change (throttled to 1 s) and every 10 s regardless.
214
347
  let last = null;
@@ -226,7 +359,7 @@ export async function runCmd(argv, io, over = {}) {
226
359
  lastWrite = now;
227
360
  const { t: _t, ...body } = last;
228
361
  try {
229
- writeRendezvous(home, { schema: RUNNER_RENDEZVOUS_SCHEMA, pid: process.pid, mode: 'paper', runId, startedAt, heartbeatAt: now, ledgerPath: files.ledger, ...body });
362
+ writeRendezvous(home, { schema: RUNNER_RENDEZVOUS_SCHEMA, pid: process.pid, mode: realMode ? 'real' : 'paper', runId, startedAt, heartbeatAt: now, ledgerPath: files.ledger, ...body });
230
363
  writeJsonAtomic(files.summary, { schema: RUN_SUMMARY_SCHEMA, runId, strategy: body.strategy, startedAt, updatedAt: now, state: body.state, book: body.book, spend: body.spend, events: body.events, ledgerHead: body.ledgerHead, source: replay ? `replay ${replay}` : 'live' });
231
364
  lastBeatOk = now;
232
365
  if (beatFailing) {
@@ -256,14 +389,23 @@ export async function runCmd(argv, io, over = {}) {
256
389
  say(`⚠ HIGH RISK strategy (${strategy.id}): one position at a time, early entries, most of them lose. Paper only; not financial advice.`);
257
390
  say(`dexterm autotrade run · ${badge} · strategy ${strategy.id} v${strategy.version} (${strategy.hash}) · ${replay ? `replay ${replay}` : 'live keyless data'} · sessions ${opts.stub ? 'stub' : local ? `ollama (local ${local.model}, profile ${local.level}, $0)` : `claude (${config.account.kind}${typeof opts.model === 'string' ? `, ${opts.model}` : ''})`}`);
258
391
  say(`run ${runId} · ledger ${files.ledger} · rendezvous ${runnerRendezvousPath(home)}`);
392
+ if (realExec && real && !('error' in real)) {
393
+ say(`REAL MODE · account ${real.check.arming.accountId} · limits ${limitsSummary(real.check.limits.values)} · channels ${enabledChannels(real.signals).join(', ')} · ttl ${Math.min(real.signals.ttlSec, real.check.limits.values.signalTtlSec ?? Infinity)} s · kill: dexterm autotrade signal kill · limits: dexterm autotrade limits show`);
394
+ const carried = realExec.inherited().length;
395
+ const elsewhere = realExec.heldElsewhere();
396
+ if (carried || elsewhere.length)
397
+ say(`REAL HOLDINGS · ${carried} carried from an earlier run of ${strategy.id} (its exits keep running)${elsewhere.length ? ` · ${elsewhere.length} held by other strategies (${elsewhere.map((h) => `${h.symbol} ${h.strategyId}`).join(', ')}): counted against the open cap, not traded here` : ''}${realExec.holdingsSource === 'rebuilt' ? ' · rebuilt from the outbox' : ''} · dexterm autotrade signal held`);
398
+ }
399
+ if (paperExec && paperLimits && !('error' in paperLimits))
400
+ say(`PAPER LIMITS · the paper scope of limits.json applies · ${limitsSummary(paperLimits.limits)} · dexterm autotrade limits show --mode paper`);
259
401
  if (host)
260
- say(`telegram: on (your own bot, ${badgeOf(host.port.network)}) · approvals ${strategy.entry.decide === 'agent+approve' ? 'required by the strategy' : 'when turned on'} · /status /pause /kill from your paired chat`);
402
+ say(`telegram: on (your own bot, ${realMode ? 'LIVE copy of each signal' : badgeOf(host.port.network)}) · approvals ${strategy.entry.decide === 'agent+approve' ? 'required by the strategy' : 'when turned on'} · /status /pause /kill from your paired chat`);
261
403
  let switchTo = null;
262
404
  let code = 0;
263
405
  try {
264
406
  const res = await runAutotrade({
265
- strategy, basis, tape, source, sessions, gate, dossier, vet, verbose: true, onLine: say, ledger, runId,
266
- ...(host ? { executor: host.port, approvals: host.service.broker } : { executor: baseExec }),
407
+ strategy, basis, tape, source, sessions, gate, dossier, vet, drain, verbose: true, onLine: say, ledger, runId,
408
+ ...(realExec ? { executor: realExec, real: realExec, ...(host ? { approvals: host.service.broker } : {}) } : paperExec ? { executor: paperExec, maxHoldMinutes: () => paperExec.maxHoldMinutes(), ...(host ? { approvals: host.service.broker } : {}) } : host ? { executor: host.port, approvals: host.service.broker } : { executor: baseExec }),
267
409
  pacing: replay ? { mode: 'paced', msPerTick: num(opts['ms-per-tick'], 2000) } : { mode: 'live' },
268
410
  // The panic file (`dexterm wallet panic`), a stop / flatten request and a local stop (signal, --max-minutes) all
269
411
  // halt: no new entries, sessions close. A signal or --max-minutes also ends the tick stream at once.
@@ -273,7 +415,7 @@ export async function runCmd(argv, io, over = {}) {
273
415
  const v = safety.step();
274
416
  return stopping && !v.halt ? { ...v, halt: 'runner-stopped', detail: 'a signal or --max-minutes' } : v;
275
417
  },
276
- onState: (st) => { runNow = st.t; last = st; writeState(st.state === 'stopped'); host?.onState(st); },
418
+ onState: (st) => { runNow = st.t; last = st; bookUsd = st.book.equityUsd; writeState(st.state === 'stopped'); host?.onState(st); },
277
419
  onFocus: (marks) => { live?.setFocus(marks); writeFocus(home, marks); },
278
420
  });
279
421
  const next = host?.takeSwitch() ?? null;
@@ -287,6 +429,35 @@ export async function runCmd(argv, io, over = {}) {
287
429
  }
288
430
  finally {
289
431
  clearInterval(beat);
432
+ if (pumpTimer)
433
+ clearInterval(pumpTimer);
434
+ if (digestTimer)
435
+ clearInterval(digestTimer);
436
+ if (listener)
437
+ await listener.close().catch(() => { });
438
+ // The runner always says goodbye: idempotent after a normal end, and the only `halted` when the run threw.
439
+ if (realExec) {
440
+ realExec.sendHalted(code ? 'runner-error' : 'runner-stopped');
441
+ // task-1679 (0.2.0 decision): what is still open when a real run ends (stop, kill, time limit, error) is impossible to miss.
442
+ const open = realExec.openOnAccount();
443
+ if (open.length) {
444
+ const line = `REAL POSITIONS STILL OPEN (${open.length}): ${open.map((h) => `${h.symbol} ${h.mint}${h.strategyId === strategy.id ? '' : ` (${h.strategyId})`}`).join(', ')}`;
445
+ say(line);
446
+ say(' these positions are still open: manage them in your executor. While no real run of the strategy that holds one is running, dexterm sends NO signal for it, sells included. The next real run of that strategy on this account carries it and its exits fire again (the "halted" signal lists them; dexterm autotrade signal held).');
447
+ if (host)
448
+ void host.service.hub.broadcast({ kind: 'text', badge: 'LIVE', lines: [line, 'these positions are still open: manage them in your executor'] }, 'real_limits').catch(() => { });
449
+ }
450
+ }
451
+ if (outbox) {
452
+ try {
453
+ await outbox.pump();
454
+ }
455
+ catch { /* pending signals stay in the outbox; they expire by TTL */ }
456
+ outbox.close();
457
+ }
458
+ real?.stop?.();
459
+ if (paperLimits && !('error' in paperLimits))
460
+ paperLimits.stop();
290
461
  process.removeListener('SIGINT', onSig);
291
462
  process.removeListener('SIGTERM', onSig);
292
463
  anchorChat('end');
@@ -335,7 +506,7 @@ function writeFocus(home, marks) {
335
506
  * task-1543: resolve the session engine. claude = the original path (null here). ollama = the user's local model,
336
507
  * allowed only when the local profile allows autotrade (paper) — checked before the run lock.
337
508
  */
338
- async function localEngine(opts, io) {
509
+ export async function localEngine(opts, io) {
339
510
  const eng = await import('../../../mcp/src/engine/index.js');
340
511
  const loaded = eng.loadAiConfig(io.env);
341
512
  const flag = typeof opts.engine === 'string' ? eng.parseEngineChoice(opts.engine) : null;
@@ -411,3 +582,86 @@ export function larpVet(g, check, timeoutMs = 20_000) {
411
582
  return { ok: true, note: `LARP score ${score}${band ? ` (${band})` : ''} ≤ ${g.maxScore}` };
412
583
  };
413
584
  }
585
+ const SOL_MINT = 'So11111111111111111111111111111111111111112';
586
+ /**
587
+ * task-1645: real mode's preconditions (realModeCheck: armed at this hash, kill off, caps, an executor channel) and
588
+ * SOL/USD for sizes in SOL: `--sol-usd N`, else the SOL pair on DexScreener (keyless), refreshed every minute.
589
+ * No SOL price, no run: a signal must state its size in SOL.
590
+ */
591
+ async function prepareReal(home, s, opts, io) {
592
+ const sc = loadSignalsConfig(home);
593
+ if (sc.problems.length)
594
+ io.stderr.write(`warning: signal channels turned off: ${sc.problems.join('; ')}\n`);
595
+ const check = realModeCheck(home, s, enabledChannels(sc.config));
596
+ if (!check.ok)
597
+ return { error: check.why };
598
+ const fixed = typeof opts['sol-usd'] === 'string' ? Number(opts['sol-usd']) : null;
599
+ if (fixed !== null && !(fixed > 1 && fixed < 100_000))
600
+ return { error: '--sol-usd needs the SOL price in USD' };
601
+ if (fixed)
602
+ return { check, signals: sc.config, solUsd: () => fixed, stop: () => { } };
603
+ const { createDataLayer, defaultDbPath } = await import('../../../data/src/index.js');
604
+ const data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env) });
605
+ let px = null;
606
+ const refresh = async () => { try {
607
+ const p = (await data.dexscreener.tokens([SOL_MINT])).get(SOL_MINT)?.pair.priceUsd ?? null;
608
+ if (p && p > 1 && p < 100_000)
609
+ px = p;
610
+ }
611
+ catch { /* keep the last price */ } };
612
+ await refresh();
613
+ if (!px) {
614
+ data.close();
615
+ return { error: 'no SOL/USD price (DexScreener did not answer); pass --sol-usd N' };
616
+ }
617
+ const t = setInterval(() => { void refresh(); }, 60_000);
618
+ t.unref?.();
619
+ return { check, signals: sc.config, solUsd: () => px, stop: () => { clearInterval(t); data.close(); } };
620
+ }
621
+ /**
622
+ * task-1654: the `paper` scope of limits.json for a paper run, or null when there is none (paper ignores the limits
623
+ * then, exactly as before). Sizes are in SOL, so it needs SOL/USD: `--sol-usd N`, else DexScreener on live data; a
624
+ * replay needs --sol-usd (today's price is not the tape's). An unreadable or invalid file is only a warning: a
625
+ * paper run moves no money, and the line says the paper scope was NOT applied.
626
+ */
627
+ async function preparePaperLimits(home, s, opts, io, replay) {
628
+ const l = loadLimits(home);
629
+ if (!l.ok) {
630
+ if (!('missing' in l && l.missing))
631
+ io.stderr.write(`warning: paper limits NOT applied: ${l.why}\n`);
632
+ return null;
633
+ }
634
+ const accountId = typeof opts.account === 'string' ? opts.account : '-';
635
+ const e = effectiveLimits(l.doc, { strategyId: s.id, accountId, mode: 'paper' });
636
+ if (!e.applies)
637
+ return null;
638
+ const fixed = typeof opts['sol-usd'] === 'string' ? Number(opts['sol-usd']) : null;
639
+ if (fixed !== null && !(fixed > 1 && fixed < 100_000))
640
+ return { error: '--sol-usd needs the SOL price in USD' };
641
+ if (fixed)
642
+ return { limits: e.values, solUsd: () => fixed, stop: () => { } };
643
+ if (replay)
644
+ return { error: 'limits.json has a paper scope (sizes in SOL): a replay needs the SOL price of its day, --sol-usd N' };
645
+ const { createDataLayer, defaultDbPath } = await import('../../../data/src/index.js');
646
+ const data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env) });
647
+ let px = null;
648
+ const refresh = async () => { try {
649
+ const p = (await data.dexscreener.tokens([SOL_MINT])).get(SOL_MINT)?.pair.priceUsd ?? null;
650
+ if (p && p > 1 && p < 100_000)
651
+ px = p;
652
+ }
653
+ catch { /* keep the last price */ } };
654
+ await refresh();
655
+ if (!px) {
656
+ data.close();
657
+ return { error: 'limits.json has a paper scope (sizes in SOL) and there is no SOL/USD price (DexScreener did not answer); pass --sol-usd N' };
658
+ }
659
+ const t = setInterval(() => { void refresh(); }, 60_000);
660
+ t.unref?.();
661
+ return { limits: e.values, solUsd: () => px, stop: () => { clearInterval(t); data.close(); } };
662
+ }
663
+ /** The local calendar day (the digest schedule's key). */
664
+ function localDayNow(t = Date.now()) {
665
+ const d = new Date(t);
666
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
667
+ }