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.
- package/dist/packages/agents/src/schema.js +22 -1
- package/dist/packages/agents/src/spectator.js +3 -1
- package/dist/packages/agents/src/story.js +11 -2
- package/dist/packages/agents/src/tiers.js +36 -2
- package/dist/packages/agents/src/tools.js +35 -2
- package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
- package/dist/packages/autotrade/assets/limits.schema.json +569 -0
- package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
- package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
- package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
- package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
- package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
- package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
- package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
- package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
- package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
- package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
- package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
- package/dist/packages/autotrade/examples/executor/README.md +70 -0
- package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
- package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
- package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
- package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
- package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
- package/dist/packages/autotrade/src/account.js +74 -0
- package/dist/packages/autotrade/src/backtest.js +358 -0
- package/dist/packages/autotrade/src/basis-text.js +32 -0
- package/dist/packages/autotrade/src/chat/approvals.js +484 -0
- package/dist/packages/autotrade/src/chat/audit.js +140 -0
- package/dist/packages/autotrade/src/chat/config.js +95 -0
- package/dist/packages/autotrade/src/chat/connector.js +24 -0
- package/dist/packages/autotrade/src/chat/control.js +104 -0
- package/dist/packages/autotrade/src/chat/fake.js +100 -0
- package/dist/packages/autotrade/src/chat/format.js +71 -0
- package/dist/packages/autotrade/src/chat/hub.js +187 -0
- package/dist/packages/autotrade/src/chat/index.js +18 -0
- package/dist/packages/autotrade/src/chat/notify.js +302 -0
- package/dist/packages/autotrade/src/chat/pairing.js +46 -0
- package/dist/packages/autotrade/src/chat/redact.js +48 -0
- package/dist/packages/autotrade/src/chat/secrets.js +106 -0
- package/dist/packages/autotrade/src/chat/service.js +251 -0
- package/dist/packages/autotrade/src/chat/telegram.js +290 -0
- package/dist/packages/autotrade/src/desk-executor.js +188 -0
- package/dist/packages/autotrade/src/engine.js +401 -0
- package/dist/packages/autotrade/src/execution-port.js +97 -0
- package/dist/packages/autotrade/src/executor-health.js +217 -0
- package/dist/packages/autotrade/src/fleet-config.js +169 -0
- package/dist/packages/autotrade/src/fleet-control.js +276 -0
- package/dist/packages/autotrade/src/fleet.js +642 -0
- package/dist/packages/autotrade/src/forward.js +355 -0
- package/dist/packages/autotrade/src/index.js +72 -0
- package/dist/packages/autotrade/src/journey.js +477 -0
- package/dist/packages/autotrade/src/ledger.js +108 -0
- package/dist/packages/autotrade/src/limits.js +682 -0
- package/dist/packages/autotrade/src/live-source.js +182 -0
- package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
- package/dist/packages/autotrade/src/monitor.js +190 -0
- package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
- package/dist/packages/autotrade/src/outbox.js +394 -0
- package/dist/packages/autotrade/src/paper-accounts.js +72 -0
- package/dist/packages/autotrade/src/paper-limits.js +87 -0
- package/dist/packages/autotrade/src/real-mode.js +688 -0
- package/dist/packages/autotrade/src/receipts.js +330 -0
- package/dist/packages/autotrade/src/replay.js +198 -0
- package/dist/packages/autotrade/src/report.js +168 -0
- package/dist/packages/autotrade/src/runner-config.js +99 -0
- package/dist/packages/autotrade/src/runner.js +923 -0
- package/dist/packages/autotrade/src/safety.js +243 -0
- package/dist/packages/autotrade/src/service-guard.js +125 -0
- package/dist/packages/autotrade/src/service-install.js +213 -0
- package/dist/packages/autotrade/src/service-recover.js +232 -0
- package/dist/packages/autotrade/src/service.js +289 -0
- package/dist/packages/autotrade/src/session.js +269 -0
- package/dist/packages/autotrade/src/sessions.js +503 -0
- package/dist/packages/autotrade/src/signal-channels.js +312 -0
- package/dist/packages/autotrade/src/signal.js +196 -0
- package/dist/packages/autotrade/src/store.js +250 -0
- package/dist/packages/autotrade/src/strategy.js +258 -0
- package/dist/packages/autotrade/src/trade-ledger.js +241 -0
- package/dist/packages/autotrade/src/yolo.js +140 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
- package/dist/packages/cli/src/ai-cmd.js +412 -0
- package/dist/packages/cli/src/analyst/backend.js +83 -15
- package/dist/packages/cli/src/analyst/codex.js +15 -9
- package/dist/packages/cli/src/analyst/ollama.js +263 -0
- package/dist/packages/cli/src/analyst/session.js +5 -2
- package/dist/packages/cli/src/app-help-gen.js +56 -8
- package/dist/packages/cli/src/autotrade-cmd/audit.js +74 -0
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +216 -0
- package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
- package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +401 -0
- package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
- package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
- package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
- package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
- package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +657 -0
- package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
- package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
- package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
- package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
- package/dist/packages/cli/src/autotrade-cmd.js +214 -0
- package/dist/packages/cli/src/display.js +156 -17
- package/dist/packages/cli/src/doctor.js +62 -3
- package/dist/packages/cli/src/export.js +10 -2
- package/dist/packages/cli/src/index.js +1 -1
- package/dist/packages/cli/src/ledger-cmd.js +290 -0
- package/dist/packages/cli/src/live/chart-fills.js +46 -0
- package/dist/packages/cli/src/live/index.js +23 -89
- package/dist/packages/cli/src/live/screens/token-live.js +9 -6
- package/dist/packages/cli/src/live/trade.js +12 -457
- package/dist/packages/cli/src/live/watchlist.js +1 -1
- package/dist/packages/cli/src/main.js +147 -190
- package/dist/packages/cli/src/mcp-cmd.js +7 -1
- package/dist/packages/cli/src/plan-cmd.js +1 -1
- package/dist/packages/cli/src/pnl-app.js +31 -21
- package/dist/packages/cli/src/pnl-card.js +162 -21
- package/dist/packages/cli/src/popup.js +22 -6
- package/dist/packages/cli/src/record-cmd.js +141 -20
- package/dist/packages/cli/src/screens/agent-hud.js +34 -2
- package/dist/packages/cli/src/screens/analyst-session.js +7 -4
- package/dist/packages/cli/src/screens/analyst.js +3 -2
- package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
- package/dist/packages/cli/src/screens/autotrade-keys.js +182 -0
- package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +776 -0
- package/dist/packages/cli/src/screens/autotrade-ui.js +650 -0
- package/dist/packages/cli/src/screens/autotrade-worker.js +91 -0
- package/dist/packages/cli/src/screens/brain.js +37 -11
- package/dist/packages/cli/src/screens/overlay.js +5 -2
- package/dist/packages/cli/src/screens/plans.js +5 -17
- package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
- package/dist/packages/cli/src/screens/positions.js +6 -28
- package/dist/packages/cli/src/screens/ticket.js +38 -205
- package/dist/packages/cli/src/screens/token.js +3 -3
- package/dist/packages/cli/src/state.js +25 -19
- package/dist/packages/cli/src/telegram-cmd.js +454 -0
- package/dist/packages/cli/src/trade.js +3 -128
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wallet-cmd.js +39 -340
- package/dist/packages/cli/src/wallet-command.js +8 -11
- package/dist/packages/cli/src/widgets.js +0 -12
- package/dist/packages/custom/src/config.js +20 -2
- package/dist/packages/custom/src/schema.js +35 -10
- package/dist/packages/custom/src/workspace.js +3 -3
- package/dist/packages/data/src/basis.js +191 -0
- package/dist/packages/data/src/detail.js +53 -2
- package/dist/packages/data/src/index.js +0 -14
- package/dist/packages/data/src/store.js +14 -5
- package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
- package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
- package/dist/packages/data/src/trade/bridge.js +4 -102
- package/dist/packages/data/src/trade/ledger/export.js +101 -0
- package/dist/packages/data/src/trade/ledger/file.js +135 -0
- package/dist/packages/data/src/trade/ledger/fold.js +191 -0
- package/dist/packages/data/src/trade/ledger/index.js +9 -0
- package/dist/packages/data/src/trade/ledger/schema.js +83 -0
- package/dist/packages/data/src/trade/ledger/sources.js +307 -0
- package/dist/packages/data/src/trade/ledger/stats.js +123 -0
- package/dist/packages/data/src/trade/ledger/tags.js +116 -0
- package/dist/packages/data/src/trade/ledger/view.js +51 -0
- package/dist/packages/data/src/trade/plans/index.js +0 -1
- package/dist/packages/data/src/trade/plans/service.js +3 -27
- package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
- package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +229 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
- package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +51 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +40 -0
- package/dist/packages/mcp/src/app-help.js +1 -1
- package/dist/packages/mcp/src/autotrade-tools.js +1107 -0
- package/dist/packages/mcp/src/backtest-requests.js +71 -0
- package/dist/packages/mcp/src/engine/config.js +159 -0
- package/dist/packages/mcp/src/engine/index.js +10 -0
- package/dist/packages/mcp/src/engine/loop.js +101 -0
- package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
- package/dist/packages/mcp/src/engine/ollama.js +258 -0
- package/dist/packages/mcp/src/engine/probe.js +110 -0
- package/dist/packages/mcp/src/engine/reason.js +35 -0
- package/dist/packages/mcp/src/engine/restrictions.js +111 -0
- package/dist/packages/mcp/src/index.js +7 -1
- package/dist/packages/mcp/src/ledger-tools.js +494 -0
- package/dist/packages/mcp/src/paper-agent.js +2 -0
- package/dist/packages/mcp/src/server.js +138 -19
- package/dist/packages/packs/src/backtest.js +3 -1
- package/dist/packages/popup/src/pages/chart-lib.js +134 -21
- package/dist/packages/popup/src/pages/chart.css +9 -0
- package/dist/packages/popup/src/pages/chart.html +3 -1
- package/dist/packages/popup/src/pages/chart.js +104 -7
- package/dist/packages/popup/src/protocol.js +2 -0
- package/dist/packages/popup/src/server.js +4 -0
- package/dist/packages/recorder/src/index.js +2 -0
- package/dist/packages/recorder/src/recorder.js +63 -6
- package/dist/packages/recorder/src/retention.js +100 -0
- package/dist/packages/recorder/src/schema.js +34 -1
- package/dist/packages/recorder/src/ticks.js +237 -0
- package/dist/packages/tui/src/index.js +1 -0
- package/dist/packages/tui/src/keymap.js +28 -1
- package/dist/packages/tui/src/markers.js +219 -0
- package/dist/packages/tui/src/widgets.js +149 -2
- package/package.json +2 -1
- package/dist/packages/data/src/trade/caps.js +0 -323
- package/dist/packages/data/src/trade/executor.js +0 -933
- package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
- package/dist/packages/data/src/trade/rpc-exec.js +0 -229
- package/dist/packages/data/src/trade/signer/client.js +0 -197
- package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
- package/dist/packages/data/src/trade/signer/host.js +0 -97
- package/dist/packages/data/src/trade/signer/main.js +0 -121
- package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
- package/dist/packages/data/src/trade/signer/protocol.js +0 -92
- package/dist/packages/data/src/trade/signer/service.js +0 -400
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
- package/dist/packages/data/src/trade/sol-usd.js +0 -86
- package/dist/packages/data/src/trade/swap-build.js +0 -108
- package/dist/packages/data/src/trade/token-delta.js +0 -201
- package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// dexterm autotrade — the strategy store (task-1503 AT-1, AUTOTRADE-SPEC §2c).
|
|
2
|
+
//
|
|
3
|
+
// ~/.dexterm/strategies/<id>.json one validated dexterm-strategy/1 document per strategy ($DEXTERM_HOME honoured)
|
|
4
|
+
// ~/.dexterm/strategies/.journal.jsonl append-only audit: {t, actor, id, ops, beforeHash, afterHash, version, …}
|
|
5
|
+
// ~/.dexterm/strategies/.backtests.json the last backtest line per strategy hash (for `list`)
|
|
6
|
+
// ~/.dexterm/strategies/.removed/ a removed strategy is moved here, never deleted
|
|
7
|
+
//
|
|
8
|
+
// Every write validates first (an invalid document is never written), is atomic (tmp + rename, 0600 in a
|
|
9
|
+
// 0700 dir) and appends one journal line naming the actor and the before/after content hashes. The shipped
|
|
10
|
+
// presets are read-only: `get` falls back to them, and editing one means `create` from it under a new id.
|
|
11
|
+
// Ids are the schema's `^[a-z][a-z0-9-]{1,31}$`, checked before any path is built, so an id cannot walk out of
|
|
12
|
+
// the directory. Nothing here decides who may change what: the model deny-list and the keypress live with
|
|
13
|
+
// the proposal flow (AT-5); this module records the actor it is told.
|
|
14
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { applyOps, dextermHome } from '../../custom/src/index.js';
|
|
18
|
+
import { fmtIssue, loadStrategyFile, validateStrategy } from "./strategy.js";
|
|
19
|
+
export const STRATEGY_ID = /^[a-z][a-z0-9-]{1,31}$/;
|
|
20
|
+
const ACTOR = /^[a-z][a-z0-9:._-]{0,63}$/;
|
|
21
|
+
/** The most ops one change may carry (the chat tool's limit, §2a). */
|
|
22
|
+
export const MAX_OPS = 20;
|
|
23
|
+
export const strategiesDir = (home) => join(home, 'strategies');
|
|
24
|
+
const fail = (msg, path = '') => ({ ok: false, errors: [{ path, msg }], warnings: [] });
|
|
25
|
+
export class StrategyStore {
|
|
26
|
+
dir;
|
|
27
|
+
journalPath;
|
|
28
|
+
now;
|
|
29
|
+
presets;
|
|
30
|
+
constructor(o = {}) {
|
|
31
|
+
this.dir = strategiesDir(o.home ?? dextermHome(o.env));
|
|
32
|
+
this.journalPath = join(this.dir, '.journal.jsonl');
|
|
33
|
+
this.now = o.now ?? Date.now;
|
|
34
|
+
this.presets = o.presetsDir ?? new URL('../assets/strategies/', import.meta.url);
|
|
35
|
+
}
|
|
36
|
+
pathOf(id) {
|
|
37
|
+
if (!STRATEGY_ID.test(id))
|
|
38
|
+
throw new Error(`bad strategy id ${JSON.stringify(id)} (lowercase letters, digits and dashes, 2-32 chars)`);
|
|
39
|
+
return join(this.dir, `${id}.json`);
|
|
40
|
+
}
|
|
41
|
+
presetFile(id) { return fileURLToPath(new URL(`${id}.json`, this.presets)); }
|
|
42
|
+
presetIds() {
|
|
43
|
+
return readdirSync(this.presets).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)).sort();
|
|
44
|
+
}
|
|
45
|
+
userIds() {
|
|
46
|
+
if (!existsSync(this.dir))
|
|
47
|
+
return [];
|
|
48
|
+
return readdirSync(this.dir).filter((f) => f.endsWith('.json') && !f.startsWith('.') && STRATEGY_ID.test(f.slice(0, -5))).map((f) => f.slice(0, -5)).sort();
|
|
49
|
+
}
|
|
50
|
+
has(id) { return existsSync(this.pathOf(id)); }
|
|
51
|
+
/** The user's file first, else the shipped preset of that id; null when neither exists. */
|
|
52
|
+
get(id) {
|
|
53
|
+
const user = this.pathOf(id);
|
|
54
|
+
if (existsSync(user))
|
|
55
|
+
return { ...loadStrategyFile(user), source: 'user', path: user };
|
|
56
|
+
if (this.presetIds().includes(id)) {
|
|
57
|
+
const p = this.presetFile(id);
|
|
58
|
+
return { ...loadStrategyFile(p), source: 'preset', path: p };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** The raw document as stored (user file, else preset), for edits. */
|
|
63
|
+
raw(id) {
|
|
64
|
+
const user = this.pathOf(id);
|
|
65
|
+
const path = existsSync(user) ? user : this.presetIds().includes(id) ? this.presetFile(id) : null;
|
|
66
|
+
if (!path)
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
list() {
|
|
76
|
+
const last = this.backtests();
|
|
77
|
+
const users = this.userIds();
|
|
78
|
+
const entry = (id, source) => {
|
|
79
|
+
const r = loadStrategyFile(source === 'user' ? this.pathOf(id) : this.presetFile(id));
|
|
80
|
+
if (!r.ok)
|
|
81
|
+
return { id, source, ok: false, errors: r.errors, warnings: r.warnings };
|
|
82
|
+
const s = r.strategy;
|
|
83
|
+
return {
|
|
84
|
+
id, source, ok: true, name: s.name, version: s.version, hash: s.hash, mode: s.mode, ...(s.highRisk ? { highRisk: true } : {}), profiles: Object.keys(s.exits.profiles), warnings: r.warnings,
|
|
85
|
+
...(last[s.hash] ? { lastBacktest: last[s.hash] } : {}),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const presets = this.presetIds();
|
|
89
|
+
return [
|
|
90
|
+
...presets.filter((id) => !users.includes(id)).map((id) => entry(id, 'preset')),
|
|
91
|
+
...users.map((id) => ({ ...entry(id, 'user'), ...(presets.includes(id) ? { shadowsPreset: true } : {}) })),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
/** Create a new user strategy from a document (its `id` names the file). Refuses an existing id. */
|
|
95
|
+
create(doc, o) {
|
|
96
|
+
const id = doc?.id;
|
|
97
|
+
if (typeof id !== 'string' || !STRATEGY_ID.test(id))
|
|
98
|
+
return fail('the document needs a valid id (lowercase letters, digits and dashes, 2-32 chars)', '/id');
|
|
99
|
+
if (this.has(id))
|
|
100
|
+
return fail(`a strategy ${id} already exists in ${this.dir} (edit it, or pick another id)`, '/id');
|
|
101
|
+
return this.write(id, doc, null, { ...o, action: 'create', ops: [{ op: 'add', path: '', value: doc }] });
|
|
102
|
+
}
|
|
103
|
+
/** Copy a preset or a stored strategy under a new id (and name). */
|
|
104
|
+
createFrom(fromId, newId, o) {
|
|
105
|
+
const base = this.raw(fromId);
|
|
106
|
+
if (!base)
|
|
107
|
+
return fail(`no strategy or preset ${fromId}`);
|
|
108
|
+
const doc = { ...base, id: newId, version: '1.0.0', ...(o.name ? { name: o.name } : {}) };
|
|
109
|
+
return this.create(doc, { actor: o.actor, why: o.why, from: fromId });
|
|
110
|
+
}
|
|
111
|
+
/** Replace a user strategy with a whole new document (same id). The version must go up. */
|
|
112
|
+
update(id, doc, o) {
|
|
113
|
+
if (!this.has(id))
|
|
114
|
+
return fail(`no user strategy ${id} (presets are read-only: create a copy first)`);
|
|
115
|
+
if (doc?.id !== id)
|
|
116
|
+
return fail(`the document id must stay ${id}`, '/id');
|
|
117
|
+
return this.write(id, doc, this.currentHash(id), { ...o, action: 'update', ops: [{ op: 'replace', path: '', value: doc }] });
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Apply JSON-patch ops (≤ MAX_OPS) to a user strategy. The patch version is bumped unless the ops set
|
|
121
|
+
* `/version` themselves; `/id` cannot change. The journal line carries the ops and their inverse.
|
|
122
|
+
*/
|
|
123
|
+
patch(id, ops, o) {
|
|
124
|
+
if (!this.has(id))
|
|
125
|
+
return fail(`no user strategy ${id} (presets are read-only: create a copy first)`);
|
|
126
|
+
if (!Array.isArray(ops) || ops.length === 0)
|
|
127
|
+
return fail('no ops');
|
|
128
|
+
if (ops.length > MAX_OPS)
|
|
129
|
+
return fail(`${ops.length} ops is more than ${MAX_OPS} in one change`);
|
|
130
|
+
if (ops.some((x) => x.path === '/id' || x.path === ''))
|
|
131
|
+
return fail('the id and the whole document cannot be patched', '/id');
|
|
132
|
+
const before = this.raw(id);
|
|
133
|
+
if (!before)
|
|
134
|
+
return fail(`${id} is not readable JSON`);
|
|
135
|
+
let next, inverse;
|
|
136
|
+
try {
|
|
137
|
+
({ doc: next, inverse } = applyOps(before, ops));
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
return fail(e.message);
|
|
141
|
+
}
|
|
142
|
+
if (!ops.some((x) => x.path === '/version'))
|
|
143
|
+
next = { ...next, version: bumpPatch(before.version) };
|
|
144
|
+
return this.write(id, next, this.currentHash(id), { ...o, action: 'patch', ops, inverse });
|
|
145
|
+
}
|
|
146
|
+
/** Move a user strategy to .removed/ (never deleted) and journal it. */
|
|
147
|
+
remove(id, o) {
|
|
148
|
+
if (!this.has(id))
|
|
149
|
+
return fail(`no user strategy ${id}`);
|
|
150
|
+
const t = this.now();
|
|
151
|
+
const before = this.currentHash(id);
|
|
152
|
+
const dest = join(this.dir, '.removed');
|
|
153
|
+
mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
154
|
+
const movedTo = join(dest, `${id}.${t}.json`);
|
|
155
|
+
renameSync(this.pathOf(id), movedTo);
|
|
156
|
+
const journal = { t, actor: checkActor(o.actor), id, action: 'remove', ops: [{ op: 'remove', path: '' }], beforeHash: before, afterHash: null, version: null, ...(o.why ? { why: o.why } : {}) };
|
|
157
|
+
this.append(journal);
|
|
158
|
+
return { ok: true, journal, movedTo };
|
|
159
|
+
}
|
|
160
|
+
journal(id) {
|
|
161
|
+
if (!existsSync(this.journalPath))
|
|
162
|
+
return [];
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const l of readFileSync(this.journalPath, 'utf8').split('\n')) {
|
|
165
|
+
if (!l.trim())
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
const j = JSON.parse(l);
|
|
169
|
+
if (!id || j.id === id)
|
|
170
|
+
out.push(j);
|
|
171
|
+
}
|
|
172
|
+
catch { /* a torn last line is skipped, never rewritten */ }
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
backtests() {
|
|
177
|
+
const p = join(this.dir, '.backtests.json');
|
|
178
|
+
if (!existsSync(p))
|
|
179
|
+
return {};
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Remember the last backtest of a strategy hash (shown by `list`; a changed strategy has a new hash). */
|
|
188
|
+
recordBacktest(line) {
|
|
189
|
+
const all = this.backtests();
|
|
190
|
+
all[line.hash] = line;
|
|
191
|
+
const keep = Object.values(all).sort((a, b) => b.t - a.t).slice(0, 200);
|
|
192
|
+
this.atomic(join(this.dir, '.backtests.json'), JSON.stringify(Object.fromEntries(keep.map((b) => [b.hash, b])), null, 2) + '\n');
|
|
193
|
+
}
|
|
194
|
+
// ───────────────────────────── internals ─────────────────────────────
|
|
195
|
+
currentHash(id) {
|
|
196
|
+
const r = loadStrategyFile(this.pathOf(id));
|
|
197
|
+
return r.ok ? r.strategy.hash : null;
|
|
198
|
+
}
|
|
199
|
+
write(id, doc, beforeHash, o) {
|
|
200
|
+
let actor;
|
|
201
|
+
try {
|
|
202
|
+
actor = checkActor(o.actor);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
return fail(e.message);
|
|
206
|
+
}
|
|
207
|
+
const r = validateStrategy(doc);
|
|
208
|
+
if (!r.ok)
|
|
209
|
+
return r;
|
|
210
|
+
const prev = this.has(id) ? this.raw(id) : null;
|
|
211
|
+
if (prev && o.action !== 'create' && !semverGreater(r.strategy.version, prev.version))
|
|
212
|
+
return { ok: false, errors: [{ path: '/version', msg: `version ${r.strategy.version} must be above the stored ${prev.version}` }], warnings: r.warnings };
|
|
213
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
214
|
+
this.atomic(this.pathOf(id), JSON.stringify(doc, null, 2) + '\n');
|
|
215
|
+
const journal = {
|
|
216
|
+
t: this.now(), actor, id, action: o.action, ops: o.ops, ...(o.inverse ? { inverse: o.inverse } : {}),
|
|
217
|
+
beforeHash, afterHash: r.strategy.hash, version: r.strategy.version,
|
|
218
|
+
...(o.why ? { why: o.why.slice(0, 400) } : {}), ...(o.proposalId ? { proposalId: o.proposalId } : {}), ...(o.from ? { from: o.from } : {}),
|
|
219
|
+
};
|
|
220
|
+
this.append(journal);
|
|
221
|
+
return { ok: true, strategy: r.strategy, warnings: r.warnings, journal };
|
|
222
|
+
}
|
|
223
|
+
append(j) {
|
|
224
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
225
|
+
appendFileSync(this.journalPath, `${JSON.stringify(j)}\n`, { mode: 0o600 });
|
|
226
|
+
}
|
|
227
|
+
atomic(path, text) {
|
|
228
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
229
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
230
|
+
writeFileSync(tmp, text, { mode: 0o600 });
|
|
231
|
+
renameSync(tmp, path);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function checkActor(a) {
|
|
235
|
+
if (typeof a !== 'string' || !ACTOR.test(a))
|
|
236
|
+
throw new Error(`bad actor ${JSON.stringify(a)}`);
|
|
237
|
+
return a;
|
|
238
|
+
}
|
|
239
|
+
export function bumpPatch(v) {
|
|
240
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
|
|
241
|
+
return m ? `${m[1]}.${m[2]}.${Number(m[3]) + 1}` : '1.0.1';
|
|
242
|
+
}
|
|
243
|
+
export function semverGreater(a, b) {
|
|
244
|
+
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
|
|
245
|
+
for (let i = 0; i < 3; i++)
|
|
246
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
247
|
+
return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
export const fmtIssues = (xs) => xs.map(fmtIssue).join('; ');
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// dexterm autotrade — the strategy document (task-1498 spike, AUTOTRADE-SPEC §1).
|
|
2
|
+
//
|
|
3
|
+
// One declarative, versioned JSON document says everything an autotrade run may do: which coins are
|
|
4
|
+
// candidates (the screener filter grammar plus safety gates and triggers), how a position is exited (exit
|
|
5
|
+
// profiles, the challenge-kit model extended with `wake`), how much loss is allowed, how a size is picked,
|
|
6
|
+
// and what the per-candidate agent session may spend. The engine, the clamp, the monitor and the backtester
|
|
7
|
+
// all read this one document, so a backtest and a paper run of the same file make the same decisions.
|
|
8
|
+
//
|
|
9
|
+
// Validation is the zero-dependency subset validator from @dexterm/custom plus the cross-field rules a
|
|
10
|
+
// schema cannot say. Nothing in here can name a key, a wallet or the signer: closed objects make an attempt
|
|
11
|
+
// to add one a precise refusal, and `mode: "live"` validates but is refused by the runner (paper only).
|
|
12
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { validate } from '../../custom/src/index.js';
|
|
15
|
+
import { parseFilter } from "../../cli/src/screener/filter.js";
|
|
16
|
+
export const STRATEGY_SCHEMA = 'dexterm-strategy/1';
|
|
17
|
+
export const EXIT_PROFILE_VERSION = '1.2.0';
|
|
18
|
+
// ───────────────────────────── schema ─────────────────────────────
|
|
19
|
+
const ID = '^[a-z][a-z0-9-]{1,31}$';
|
|
20
|
+
const SEMVER = '^[0-9]+\\.[0-9]+\\.[0-9]+$';
|
|
21
|
+
const num = (min, max, int = false) => ({ type: 'number', min, max, int });
|
|
22
|
+
const str = (max, pattern) => ({ type: 'string', max, pattern });
|
|
23
|
+
const obj = (props, required = Object.keys(props)) => ({ type: 'object', additional: false, required, props });
|
|
24
|
+
const nullable = (n) => ({ oneOf: [{ type: 'null' }, n] });
|
|
25
|
+
const ageRange = obj({ min: num(0, 8760), max: nullable(num(0, 8760)) });
|
|
26
|
+
export const SCHEMAS = {
|
|
27
|
+
exitProfile: obj({
|
|
28
|
+
kind: { type: 'string', enum: ['exit-profile'] },
|
|
29
|
+
id: str(32, ID), name: str(40), version: str(16, SEMVER), description: str(400),
|
|
30
|
+
fit: obj({ summary: str(300), minLiquidityUsd: num(1000), ageHours: ageRange, maxTop10Pct: nullable(num(0, 100)), signals: { type: 'array', items: str(200), max: 8 } }, ['summary', 'minLiquidityUsd', 'ageHours']),
|
|
31
|
+
sizing: obj({ maxSizePctOfBankroll: num(0.1, 25) }),
|
|
32
|
+
tpLadder: { type: 'array', max: 6, items: obj({ atPct: num(0.1, 10000), sellFrac: num(0.01, 1) }) },
|
|
33
|
+
trailingStop: nullable(obj({ armAtPct: num(0), trailPct: num(5, 60) })),
|
|
34
|
+
timeStop: nullable(obj({ minutes: num(1, 10080), ifBelowPct: num(-100, 10000), action: { type: 'string', enum: ['exit', 'review', 'tighten'] } })),
|
|
35
|
+
hardStop: obj({ enabled: { type: 'boolean' }, atPct: nullable(num(-90, 0)), why: str(400) }),
|
|
36
|
+
thesisBreak: obj({
|
|
37
|
+
liquidityDropPct: num(5, 90), liquidityWindowMin: num(1, 60), mcapDivergenceX: nullable(num(1.5)),
|
|
38
|
+
devSell: obj({ exitOnAnyDevSell: { type: 'boolean' }, topHolderSellPctOfBag: nullable(num(1, 100)) }),
|
|
39
|
+
volumeDeath: nullable(obj({ m5TradesBelow: num(1, 10000, true), forMinutes: num(1, 1440) })),
|
|
40
|
+
}),
|
|
41
|
+
reentry: obj({ allowed: { type: 'boolean' }, cooldownMinutes: num(0, 10080), maxEntriesPerCoinPerSession: num(1, 5, true), requires: str(300) }),
|
|
42
|
+
carry: obj({ allowed: { type: 'boolean' }, requiresTpFilled: num(0, 6, true), maxRemainderPctOfBankroll: num(0, 25) }),
|
|
43
|
+
reprofileTo: { type: 'array', max: 8, items: str(32, ID) },
|
|
44
|
+
wake: obj({ onTpFilled: { type: 'boolean' }, onTimeReview: { type: 'boolean' }, drawdownPct: nullable(num(-95, -1)), thesisWarnFrac: nullable(num(0.1, 0.95)), noAnswer: { type: 'string', enum: ['hold', 'exit'] }, onCostRecovered: { type: 'boolean' } }, ['onTpFilled', 'onTimeReview', 'drawdownPct', 'thesisWarnFrac', 'noAnswer']),
|
|
45
|
+
costRecovery: nullable(obj({ atPct: num(10, 10000), maxSellFrac: num(0.1, 0.9) })),
|
|
46
|
+
bagTrail: nullable(obj({ trailPct: num(10, 90), armAtPct: num(0, 100000) }, ['trailPct'])),
|
|
47
|
+
maxHoldMinutes: nullable(num(1, 43200)),
|
|
48
|
+
notes: { type: 'array', max: 8, items: str(300) },
|
|
49
|
+
}, ['kind', 'id', 'name', 'version', 'description', 'fit', 'sizing', 'tpLadder', 'trailingStop', 'timeStop', 'hardStop', 'thesisBreak', 'reentry', 'carry', 'reprofileTo']),
|
|
50
|
+
trigger: {
|
|
51
|
+
oneOf: [
|
|
52
|
+
obj({ kind: { type: 'string', enum: ['screen'] } }),
|
|
53
|
+
obj({ kind: { type: 'string', enum: ['momentum'] }, m5MinPct: num(-100, 10000), buySellMin: num(0, 100) }),
|
|
54
|
+
obj({ kind: { type: 'string', enum: ['narrative'] }, minHeat: num(0, 100) }),
|
|
55
|
+
obj({ kind: { type: 'string', enum: ['wallet'] }, list: { type: 'string', enum: ['followed'] }, minBuyUsd: num(0, 1e7) }),
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
strategy: obj({
|
|
59
|
+
kind: { type: 'string', enum: ['strategy'] },
|
|
60
|
+
schema: { type: 'string', enum: [STRATEGY_SCHEMA] },
|
|
61
|
+
id: str(32, ID), name: str(40), version: str(16, SEMVER), description: str(600),
|
|
62
|
+
mode: { type: 'string', enum: ['paper', 'real', 'live'] },
|
|
63
|
+
highRisk: { type: 'boolean' },
|
|
64
|
+
bankroll: obj({ startUsd: num(1, 1e7), source: { type: 'string', enum: ['fixed', 'desk'] } }, ['startUsd']),
|
|
65
|
+
entry: obj({
|
|
66
|
+
screen: str(400),
|
|
67
|
+
ageHours: ageRange,
|
|
68
|
+
safety: obj({
|
|
69
|
+
maxTop10Pct: nullable(num(0, 100)), maxDevPct: nullable(num(0, 100)),
|
|
70
|
+
requireNoMintAuthority: { type: 'boolean' }, requireNoFreezeAuthority: { type: 'boolean' },
|
|
71
|
+
maxMcapSplitX: nullable(num(1.5, 100)),
|
|
72
|
+
youngVertical: nullable(obj({ maxAgeHours: num(0, 168), maxH1Pct: num(0, 100000) })),
|
|
73
|
+
unknownHolders: { type: 'string', enum: ['reject', 'allow'] },
|
|
74
|
+
larp: nullable(obj({ maxScore: num(0, 100), unknown: { type: 'string', enum: ['reject', 'allow'] } })),
|
|
75
|
+
}, ['maxTop10Pct', 'maxDevPct', 'requireNoMintAuthority', 'requireNoFreezeAuthority', 'maxMcapSplitX', 'youngVertical', 'unknownHolders']),
|
|
76
|
+
triggers: { type: 'array', max: 8, items: 'trigger' },
|
|
77
|
+
triggerMode: { type: 'string', enum: ['any', 'all'] },
|
|
78
|
+
decide: { type: 'string', enum: ['agent', 'auto', 'agent+approve'] },
|
|
79
|
+
candidateTtlSec: num(10, 3600),
|
|
80
|
+
}),
|
|
81
|
+
exits: obj({ default: str(32, ID), profiles: { type: 'array', max: 8, items: { oneOf: [str(48, '^builtin:[a-z][a-z0-9-]{1,31}$'), 'exitProfile'] } } }),
|
|
82
|
+
sizing: obj({
|
|
83
|
+
mode: { type: 'string', enum: ['fixed', 'pct', 'risk', 'confidence'] },
|
|
84
|
+
fixedUsd: num(1, 1e6), pct: num(0.1, 25), riskPct: num(0.1, 10),
|
|
85
|
+
confidence: obj({ minPct: num(0.1, 25), maxPct: num(0.1, 25), default: num(0, 1) }),
|
|
86
|
+
ladder: { type: 'array', max: 4, items: obj({ frac: num(0.05, 1), atPct: num(-90, 1000), withinMin: num(1, 1440) }) },
|
|
87
|
+
maxPctOfBankroll: num(0.1, 25), maxUsd: num(1, 1e6), minUsd: num(0.01, 1e6),
|
|
88
|
+
}, ['mode', 'maxPctOfBankroll', 'maxUsd', 'minUsd']),
|
|
89
|
+
risk: obj({
|
|
90
|
+
perTrade: obj({ maxLossPctOfBankroll: num(0.1, 25), assumedLossPctNoStop: num(10, 100) }),
|
|
91
|
+
dailyLossHaltPct: num(1, 100),
|
|
92
|
+
drawdown: obj({ sizeDownAtPct: num(1, 100), sizeDownMult: num(0.05, 1), recoverBelowPct: num(0, 100), haltAtPct: num(1, 100) }),
|
|
93
|
+
maxConcurrent: num(1, 20, true), maxExposurePct: num(1, 100),
|
|
94
|
+
lossStreak: obj({ losses: num(1, 50, true), cooldownMin: num(0, 10080) }),
|
|
95
|
+
maxEntriesPerDay: num(1, 500, true),
|
|
96
|
+
perCoin: obj({ maxEntries: num(1, 20, true), cooldownMin: num(0, 10080) }),
|
|
97
|
+
recoveredBags: obj({ countAsOpen: { type: 'boolean' }, maxBags: num(0, 50, true) }),
|
|
98
|
+
}, ['perTrade', 'dailyLossHaltPct', 'drawdown', 'maxConcurrent', 'maxExposurePct', 'lossStreak', 'maxEntriesPerDay', 'perCoin']),
|
|
99
|
+
agent: obj({
|
|
100
|
+
model: str(64, '^[a-z0-9][a-z0-9.-]{1,63}$'), maxSessions: num(1, 8, true),
|
|
101
|
+
budgetUsdPerSession: num(0.01, 50), budgetUsdPerDay: num(0.01, 500), maxTurns: num(1, 1000, true),
|
|
102
|
+
deltaEverySec: num(1, 600), decideWithinSec: num(5, 3600), sessionMaxMin: num(1, 1440),
|
|
103
|
+
onTimeout: { type: 'string', enum: ['skip'] },
|
|
104
|
+
respawnOnWake: { type: 'boolean' },
|
|
105
|
+
}, ['model', 'maxSessions', 'budgetUsdPerSession', 'budgetUsdPerDay', 'maxTurns', 'deltaEverySec', 'decideWithinSec', 'sessionMaxMin', 'onTimeout']),
|
|
106
|
+
execution: obj({ slippageBps: num(0, 5000), feeBps: num(0, 1000), networkFeeUsd: num(0, 10), latencyMs: num(0, 120000), maxPriceImpactPct: num(0.1, 50), minExitLiquidityUsd: num(0, 1e7) }),
|
|
107
|
+
notes: { type: 'array', max: 8, items: str(300) },
|
|
108
|
+
}, ['kind', 'schema', 'id', 'name', 'version', 'description', 'mode', 'bankroll', 'entry', 'exits', 'sizing', 'risk', 'agent', 'execution']),
|
|
109
|
+
};
|
|
110
|
+
/** Schema + the cross-field rules for one exit profile. */
|
|
111
|
+
export function validateExitProfile(p, at = '') {
|
|
112
|
+
const errs = validate('exitProfile', p, at, [], SCHEMAS);
|
|
113
|
+
if (errs.length)
|
|
114
|
+
return errs;
|
|
115
|
+
const x = p;
|
|
116
|
+
for (let i = 1; i < x.tpLadder.length; i++)
|
|
117
|
+
if (!(x.tpLadder[i].atPct > x.tpLadder[i - 1].atPct))
|
|
118
|
+
errs.push({ path: `${at}/tpLadder/${i}`, msg: 'atPct must be strictly ascending' });
|
|
119
|
+
const sum = x.tpLadder.reduce((s, t) => s + t.sellFrac, 0);
|
|
120
|
+
if (sum > 1 + 1e-9)
|
|
121
|
+
errs.push({ path: `${at}/tpLadder`, msg: `sellFrac adds up to ${round(sum, 4)} (more than the whole position)` });
|
|
122
|
+
if (x.hardStop.enabled && x.hardStop.atPct === null)
|
|
123
|
+
errs.push({ path: `${at}/hardStop/atPct`, msg: 'an enabled hard stop needs atPct' });
|
|
124
|
+
if (x.carry.allowed && x.carry.maxRemainderPctOfBankroll <= 0)
|
|
125
|
+
errs.push({ path: `${at}/carry`, msg: 'carry allowed needs maxRemainderPctOfBankroll > 0' });
|
|
126
|
+
if (x.bagTrail && !x.costRecovery)
|
|
127
|
+
errs.push({ path: `${at}/bagTrail`, msg: 'a bag trail is armed by the cost-recovery sell: set costRecovery' });
|
|
128
|
+
if (x.costRecovery && x.tpLadder.length && x.tpLadder[0].atPct <= x.costRecovery.atPct)
|
|
129
|
+
errs.push({ path: `${at}/tpLadder/0`, msg: 'a TP step at or below the cost-recovery level would sell first: put the ladder above costRecovery.atPct' });
|
|
130
|
+
if (x.reprofileTo.includes(x.id))
|
|
131
|
+
errs.push({ path: `${at}/reprofileTo`, msg: 'a profile cannot re-profile to itself' });
|
|
132
|
+
return errs;
|
|
133
|
+
}
|
|
134
|
+
/** Load the shipped exit-profile library (assets/autotrade/profiles). */
|
|
135
|
+
export function builtinProfiles(dir = new URL('../assets/profiles/', import.meta.url)) {
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith('.json')).sort()) {
|
|
138
|
+
const p = JSON.parse(readFileSync(new URL(f, dir), 'utf8'));
|
|
139
|
+
const errs = validateExitProfile(p);
|
|
140
|
+
if (errs.length)
|
|
141
|
+
throw new Error(`builtin profile ${f} is invalid: ${errs.map(fmtIssue).join('; ')}`);
|
|
142
|
+
out[p.id] = p;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Validate a strategy document and resolve its profile references. Never throws on bad input: every
|
|
148
|
+
* problem comes back as a path + message so the TUI editor and the assistant's tool can show it inline.
|
|
149
|
+
*/
|
|
150
|
+
export function validateStrategy(doc, library = builtinProfiles()) {
|
|
151
|
+
const warnings = [];
|
|
152
|
+
const errors = validate('strategy', doc, '', [], SCHEMAS);
|
|
153
|
+
if (errors.length)
|
|
154
|
+
return { ok: false, errors, warnings };
|
|
155
|
+
const s = doc;
|
|
156
|
+
const f = parseFilter(s.entry.screen);
|
|
157
|
+
if (!f.ok)
|
|
158
|
+
errors.push({ path: '/entry/screen', msg: `${f.error} (column ${f.at})` });
|
|
159
|
+
const profiles = {};
|
|
160
|
+
s.exits.profiles.forEach((ref, i) => {
|
|
161
|
+
const p = typeof ref === 'string' ? library[ref.slice('builtin:'.length)] : ref;
|
|
162
|
+
if (!p) {
|
|
163
|
+
errors.push({ path: `/exits/profiles/${i}`, msg: `unknown builtin profile ${String(ref)}` });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
errors.push(...validateExitProfile(p, `/exits/profiles/${i}`));
|
|
167
|
+
if (profiles[p.id])
|
|
168
|
+
errors.push({ path: `/exits/profiles/${i}`, msg: `duplicate profile id ${p.id}` });
|
|
169
|
+
profiles[p.id] = p;
|
|
170
|
+
});
|
|
171
|
+
if (!profiles[s.exits.default])
|
|
172
|
+
errors.push({ path: '/exits/default', msg: `default profile ${s.exits.default} is not in exits.profiles` });
|
|
173
|
+
for (const p of Object.values(profiles)) {
|
|
174
|
+
for (const t of p.reprofileTo)
|
|
175
|
+
if (!profiles[t])
|
|
176
|
+
warnings.push({ path: '/exits/profiles', msg: `${p.id} may re-profile to ${t}, which this strategy does not include (that edge is unused)` });
|
|
177
|
+
if (p.sizing.maxSizePctOfBankroll < s.sizing.maxPctOfBankroll)
|
|
178
|
+
warnings.push({ path: '/sizing/maxPctOfBankroll', msg: `profile ${p.id} caps a position at ${p.sizing.maxSizePctOfBankroll}% (the lower cap wins)` });
|
|
179
|
+
}
|
|
180
|
+
const z = s.sizing;
|
|
181
|
+
const need = (k, mode) => { if (z.mode === mode && z[k] === undefined)
|
|
182
|
+
errors.push({ path: `/sizing/${k}`, msg: `sizing mode ${mode} needs ${k}` }); };
|
|
183
|
+
need('fixedUsd', 'fixed');
|
|
184
|
+
need('pct', 'pct');
|
|
185
|
+
need('riskPct', 'risk');
|
|
186
|
+
need('confidence', 'confidence');
|
|
187
|
+
if (z.confidence && z.confidence.minPct > z.confidence.maxPct)
|
|
188
|
+
errors.push({ path: '/sizing/confidence', msg: 'minPct is above maxPct' });
|
|
189
|
+
if (z.minUsd > z.maxUsd)
|
|
190
|
+
errors.push({ path: '/sizing/minUsd', msg: 'minUsd is above maxUsd' });
|
|
191
|
+
if (z.ladder) {
|
|
192
|
+
const sum = z.ladder.reduce((a, t) => a + t.frac, 0);
|
|
193
|
+
if (Math.abs(sum - 1) > 1e-6)
|
|
194
|
+
errors.push({ path: '/sizing/ladder', msg: `tranche fractions add up to ${round(sum, 4)}, they must add up to 1` });
|
|
195
|
+
if (z.ladder[0] && z.ladder[0].atPct !== 0)
|
|
196
|
+
errors.push({ path: '/sizing/ladder/0/atPct', msg: 'the first tranche fills at entry (atPct 0)' });
|
|
197
|
+
}
|
|
198
|
+
const r = s.risk;
|
|
199
|
+
if (r.drawdown.sizeDownAtPct > r.drawdown.haltAtPct)
|
|
200
|
+
errors.push({ path: '/risk/drawdown', msg: 'sizeDownAtPct must not exceed haltAtPct' });
|
|
201
|
+
if (r.drawdown.recoverBelowPct >= r.drawdown.sizeDownAtPct)
|
|
202
|
+
errors.push({ path: '/risk/drawdown/recoverBelowPct', msg: 'recoverBelowPct must be below sizeDownAtPct' });
|
|
203
|
+
if (s.agent.budgetUsdPerSession > s.agent.budgetUsdPerDay)
|
|
204
|
+
errors.push({ path: '/agent/budgetUsdPerSession', msg: 'a session budget above the daily budget' });
|
|
205
|
+
if (s.entry.ageHours.max !== null && s.entry.ageHours.max < s.entry.ageHours.min)
|
|
206
|
+
errors.push({ path: '/entry/ageHours', msg: 'max is below min' });
|
|
207
|
+
if (s.entry.triggers.length === 0)
|
|
208
|
+
errors.push({ path: '/entry/triggers', msg: 'at least one trigger (use {"kind":"screen"} for "every screen match")' });
|
|
209
|
+
for (const [i, t] of s.entry.triggers.entries())
|
|
210
|
+
if (t.kind === 'narrative' || t.kind === 'wallet')
|
|
211
|
+
warnings.push({ path: `/entry/triggers/${i}`, msg: `${t.kind} triggers need the live ${t.kind} feed; replays and backtests treat them as not firing` });
|
|
212
|
+
if (s.bankroll.source === 'desk')
|
|
213
|
+
warnings.push({ path: '/bankroll/source', msg: 'desk reads the paper desk book during a run; backtests use startUsd' });
|
|
214
|
+
if (s.entry.safety.larp)
|
|
215
|
+
warnings.push({ path: '/entry/safety/larp', msg: 'the LARP gate needs the live web (live data runs only); replays and backtests do not evaluate it' });
|
|
216
|
+
if (s.mode === 'real')
|
|
217
|
+
warnings.push({ path: '/mode', msg: 'mode real sends a LIVE TRADE FIRE signal for every trade to your own executor; it runs only after `dexterm autotrade signal arm` on your own terminal, at this exact version' });
|
|
218
|
+
if (s.mode === 'live')
|
|
219
|
+
warnings.push({ path: '/mode', msg: 'mode live does not run: dexterm runs strategies on paper only (it holds no key and places no live order); live trading happens in the Telegram trading bot of your choice' });
|
|
220
|
+
if (errors.length)
|
|
221
|
+
return { ok: false, errors, warnings };
|
|
222
|
+
const resolved = { ...s, exits: { default: s.exits.default, profiles } };
|
|
223
|
+
return { ok: true, strategy: { ...resolved, hash: strategyHash(resolved) }, warnings };
|
|
224
|
+
}
|
|
225
|
+
/** Stable content hash (sorted keys) written into every audit line so a decision names its exact rules. */
|
|
226
|
+
export function strategyHash(s) {
|
|
227
|
+
return createHash('sha256').update(canonical(s)).digest('hex').slice(0, 16);
|
|
228
|
+
}
|
|
229
|
+
export function canonical(v) {
|
|
230
|
+
if (v === null || typeof v !== 'object')
|
|
231
|
+
return JSON.stringify(v);
|
|
232
|
+
if (Array.isArray(v))
|
|
233
|
+
return `[${v.map(canonical).join(',')}]`;
|
|
234
|
+
const o = v;
|
|
235
|
+
return `{${Object.keys(o).filter((k) => o[k] !== undefined && k !== 'hash').sort().map((k) => `${JSON.stringify(k)}:${canonical(o[k])}`).join(',')}}`;
|
|
236
|
+
}
|
|
237
|
+
export function loadStrategyFile(path, library) {
|
|
238
|
+
let doc;
|
|
239
|
+
try {
|
|
240
|
+
doc = JSON.parse(readFileSync(path, 'utf8'));
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
return { ok: false, errors: [{ path: '', msg: `not JSON: ${e.message}` }], warnings: [] };
|
|
244
|
+
}
|
|
245
|
+
return validateStrategy(doc, library);
|
|
246
|
+
}
|
|
247
|
+
/** The shipped presets (assets/autotrade/strategies). */
|
|
248
|
+
export function presetPath(id) {
|
|
249
|
+
return new URL(`../assets/strategies/${id}.json`, import.meta.url);
|
|
250
|
+
}
|
|
251
|
+
export function loadPreset(id) {
|
|
252
|
+
const r = loadStrategyFile(presetPath(id));
|
|
253
|
+
if (!r.ok)
|
|
254
|
+
throw new Error(`preset ${id} is invalid: ${r.errors.map(fmtIssue).join('; ')}`);
|
|
255
|
+
return r.strategy;
|
|
256
|
+
}
|
|
257
|
+
export const fmtIssue = (i) => `${i.path || '/'}: ${i.msg}`;
|
|
258
|
+
export const round = (x, dp = 2) => Math.round(x * 10 ** dp) / 10 ** dp;
|