dexterm-cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agents/src/schema.js +22 -1
- package/dist/packages/agents/src/story.js +11 -2
- package/dist/packages/agents/src/tiers.js +27 -2
- package/dist/packages/agents/src/tools.js +30 -2
- package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -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/src/account.js +74 -0
- package/dist/packages/autotrade/src/backtest.js +357 -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 +93 -0
- package/dist/packages/autotrade/src/chat/connector.js +24 -0
- package/dist/packages/autotrade/src/chat/control.js +103 -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 +292 -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 +99 -0
- package/dist/packages/autotrade/src/chat/service.js +248 -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 +387 -0
- package/dist/packages/autotrade/src/execution-port.js +97 -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 +639 -0
- package/dist/packages/autotrade/src/index.js +53 -0
- package/dist/packages/autotrade/src/ledger.js +108 -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/paper-accounts.js +72 -0
- package/dist/packages/autotrade/src/replay.js +198 -0
- package/dist/packages/autotrade/src/runner-config.js +99 -0
- package/dist/packages/autotrade/src/runner.js +872 -0
- package/dist/packages/autotrade/src/safety.js +243 -0
- package/dist/packages/autotrade/src/session.js +269 -0
- package/dist/packages/autotrade/src/sessions.js +503 -0
- package/dist/packages/autotrade/src/store.js +250 -0
- package/dist/packages/autotrade/src/strategy.js +256 -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 +82 -15
- package/dist/packages/cli/src/analyst/codex.js +14 -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 +44 -8
- package/dist/packages/cli/src/autotrade-cmd/audit.js +57 -0
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +199 -0
- package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +397 -0
- package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +413 -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 +168 -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 +288 -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 +158 -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-keys.js +169 -0
- package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +725 -0
- package/dist/packages/cli/src/screens/autotrade-ui.js +640 -0
- package/dist/packages/cli/src/screens/autotrade-worker.js +89 -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 +451 -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-parallel.md +42 -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 +217 -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 +49 -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 +37 -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 +35 -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-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 +43 -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-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-live-trading-changed-in-0-1-7.md +26 -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 +33 -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-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 +38 -0
- package/dist/packages/mcp/src/app-help.js +1 -1
- package/dist/packages/mcp/src/autotrade-tools.js +944 -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 +346 -0
- package/dist/packages/mcp/src/paper-agent.js +2 -0
- package/dist/packages/mcp/src/server.js +127 -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 +21 -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,188 @@
|
|
|
1
|
+
// dexterm autotrade — the paper-desk executor (task-1506 AT-4, AUTOTRADE-SPEC §5, §6.1). PAPER ONLY.
|
|
2
|
+
//
|
|
3
|
+
// Autotrade's orders through the app's existing paper desk (PaperExitDesk: the PaperAgentDesk plus exit plans)
|
|
4
|
+
// instead of the backtest fill model, so the agent
|
|
5
|
+
// path inherits every fence the desk already has: the armed mandate's caps at the agent tier (hands-free × 0.25),
|
|
6
|
+
// the panic file, the control file, the keyless Jupiter quote with its bounded retry, and the hash-chained
|
|
7
|
+
// journal. The strategy clamp has already sized the order; the desk's caps sit around it and both apply.
|
|
8
|
+
//
|
|
9
|
+
// buy() an entry or a ladder add. Refused unless the mandate is armed AND paper-only, and refused while the
|
|
10
|
+
// position monitor's heartbeat is stale (the monitor must be up to run the exits). Every entry fill
|
|
11
|
+
// registers its desk-side exit plan (P1), and carries a note (P4) naming the run, position and profile.
|
|
12
|
+
// sell() a mechanical or agent exit, journaled by the desk as agent.paper.exit.<rule>.
|
|
13
|
+
// costRecoverySellUsd() the proceeds curve a cost-recovery sell is sized on: a linear-impact estimate from one
|
|
14
|
+
// quote of the largest allowed slice, one confirming quote at the estimate, and at most one correction:
|
|
15
|
+
// never more than 3 quotes, inside the keyless budget.
|
|
16
|
+
// book() the desk's book in USD, for `bankroll.source: desk`.
|
|
17
|
+
//
|
|
18
|
+
// Nothing here can sign, send or touch a key: the desk only accepts a paper executor, and this file imports
|
|
19
|
+
// types only from the data layer (a refusal is recognised by its error name).
|
|
20
|
+
export const DESK_EXIT_OWNER = 'autotrade-monitor';
|
|
21
|
+
/** Most quotes a cost-recovery slice is sized on (the fill then takes its own fresh quote, as every desk fill does). */
|
|
22
|
+
export const MAX_SIZING_QUOTES = 3;
|
|
23
|
+
const errReason = (e) => (e instanceof Error && e.name === 'PaperDeskError' ? `desk-${e.reason}` : `desk-error:${e instanceof Error ? e.message.slice(0, 120) : String(e)}`);
|
|
24
|
+
const r6 = (x) => Number(x.toPrecision(6));
|
|
25
|
+
export class DeskExecutor {
|
|
26
|
+
name = 'paper-desk';
|
|
27
|
+
/** The monitor process owns the exits of every position this executor opens (a runner must not run them inline). */
|
|
28
|
+
exitsByMonitor = true;
|
|
29
|
+
d;
|
|
30
|
+
now;
|
|
31
|
+
seq = 0;
|
|
32
|
+
constructor(deps) {
|
|
33
|
+
this.d = deps;
|
|
34
|
+
this.now = deps.now ?? Date.now;
|
|
35
|
+
}
|
|
36
|
+
/** Why an entry may not go to the desk right now, or null. */
|
|
37
|
+
entryBlock() {
|
|
38
|
+
const m = this.d.mandate();
|
|
39
|
+
if (!m)
|
|
40
|
+
return 'no-mandate';
|
|
41
|
+
if (m.paperOnly !== true)
|
|
42
|
+
return 'mandate-not-paper-only';
|
|
43
|
+
if (m.status !== 'armed' || !m.armedBy)
|
|
44
|
+
return 'mandate-not-armed';
|
|
45
|
+
if (!this.d.monitorAlive())
|
|
46
|
+
return 'monitor-heartbeat-stale';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
held(mint) {
|
|
50
|
+
return this.d.desk.positions().find((p) => p.mint === mint && p.tokens > 0) ?? null;
|
|
51
|
+
}
|
|
52
|
+
toFill(k, rec, reason) {
|
|
53
|
+
const solUsd = this.d.solUsd() ?? (rec.priceUsd && rec.priceSol ? rec.priceUsd / rec.priceSol : 0);
|
|
54
|
+
const usd = (Math.abs(Number(rec.solDelta)) / 1e9) * solUsd;
|
|
55
|
+
const tokens = Math.abs(Number(rec.tokenDelta)) / 10 ** rec.decimals;
|
|
56
|
+
return {
|
|
57
|
+
t: rec.t, side: rec.side, usd: r6(usd), tokens, price: k.price ?? (rec.priceSol * solUsd), avgPrice: tokens > 0 ? r6(usd / tokens) : 0,
|
|
58
|
+
impactPct: rec.priceImpactPct ?? 0, partial: false, reason,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** An entry (or a ladder add) of `usd` through the desk, with its exit plan registered on the first fill. */
|
|
62
|
+
async buy(k, usd, ctx = {}) {
|
|
63
|
+
const block = this.entryBlock();
|
|
64
|
+
if (block)
|
|
65
|
+
return { refused: block };
|
|
66
|
+
const decimals = this.d.decimals(k.mint);
|
|
67
|
+
if (decimals === null)
|
|
68
|
+
return { refused: 'decimals-unknown' };
|
|
69
|
+
const kind = ctx.kind ?? (this.held(k.mint) ? 'add' : 'open');
|
|
70
|
+
const profile = ctx.profile ?? this.d.defaultProfile;
|
|
71
|
+
const positionId = ctx.positionId ?? `${this.d.runId ?? 'run'}:${k.mint.slice(0, 8)}:${this.now().toString(36)}${++this.seq}`;
|
|
72
|
+
const note = {
|
|
73
|
+
by: 'autotrade', kind, positionId, profile, runId: this.d.runId ?? null, sessionId: ctx.sessionId ?? null, mid: k.price,
|
|
74
|
+
...(this.d.strategy ? { strategy: this.d.strategy.id, strategyHash: this.d.strategy.hash } : {}),
|
|
75
|
+
...(ctx.reason ? { reason: ctx.reason.slice(0, 200) } : {}),
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
const t = this.d.desk.open({ mint: k.mint, symbol: k.symbol, decimals, side: 'buy' });
|
|
79
|
+
this.d.desk.size(t.id, { usd });
|
|
80
|
+
const { fill } = await this.d.desk.confirm(t.id, { note });
|
|
81
|
+
if (kind === 'open')
|
|
82
|
+
this.d.desk.registerExitPlan({ mint: k.mint, owner: DESK_EXIT_OWNER, positionId, profileId: profile, runId: this.d.runId ?? null, note: { sessionId: ctx.sessionId ?? null } });
|
|
83
|
+
return this.toFill(k, fill, kind === 'open' ? 'entry' : 'ladder-add');
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return { refused: errReason(e) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Sell `tokens` of the held position as exit rule `reason` (journaled agent.paper.exit.<reason>). */
|
|
90
|
+
async sell(k, tokens, reason, ctx = {}) {
|
|
91
|
+
const pos = this.held(k.mint);
|
|
92
|
+
if (!pos)
|
|
93
|
+
return { refused: 'no-position' };
|
|
94
|
+
const fraction = Math.min(1, tokens / pos.tokens);
|
|
95
|
+
if (!(fraction > 0))
|
|
96
|
+
return { refused: 'nothing-to-sell' };
|
|
97
|
+
try {
|
|
98
|
+
const r = await this.d.desk.exit({
|
|
99
|
+
mint: k.mint, fraction: fraction >= 0.999999 ? 1 : fraction, rule: reason, detail: ctx.detail,
|
|
100
|
+
note: { by: 'autotrade', runId: this.d.runId ?? null, ...(ctx.positionId ? { positionId: ctx.positionId } : {}), ...(ctx.agentReason ? { agentReason: ctx.agentReason.slice(0, 200) } : {}) },
|
|
101
|
+
});
|
|
102
|
+
return this.toFill(k, r.fill, reason);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return { refused: errReason(e) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The proceeds curve for a cost-recovery sell, from at most MAX_SIZING_QUOTES desk quotes (spec §9 AT-4):
|
|
110
|
+
* 1. quote the largest allowed slice (maxSellFrac of the bag): proceeds P at size C;
|
|
111
|
+
* 2. a linear-impact model through it (proceeds(t) = t·m·(1 − a·t), m = the tick's mid price) gives the
|
|
112
|
+
* slice t* that nets the owed cost; quote t* to confirm;
|
|
113
|
+
* 3. if the confirmation is short, scale once (t3 = t*·owed/proceeds(t*), at most C) and quote that.
|
|
114
|
+
* Returns a monotone piecewise-linear `sellUsd(tokens)` through the quoted points for monitorTick's search, or
|
|
115
|
+
* null when no quote could be had (the monitor then waits for the next tick: a cost-recovery sell is never
|
|
116
|
+
* sized on a guess).
|
|
117
|
+
*/
|
|
118
|
+
async costRecoverySellUsd(p, prof, k) {
|
|
119
|
+
const cr = prof.costRecovery;
|
|
120
|
+
const solUsd = this.d.solUsd();
|
|
121
|
+
if (!cr || !solUsd || !k.price || p.tokens <= 0)
|
|
122
|
+
return null;
|
|
123
|
+
const owed = p.costUsd - p.realizedUsd;
|
|
124
|
+
const cap = p.tokens * cr.maxSellFrac;
|
|
125
|
+
let quotes = 0;
|
|
126
|
+
const quote = async (tokens) => {
|
|
127
|
+
if (quotes >= MAX_SIZING_QUOTES)
|
|
128
|
+
return null;
|
|
129
|
+
quotes++;
|
|
130
|
+
try {
|
|
131
|
+
const q = await this.d.desk.previewSell({ mint: p.mint, fraction: Math.min(1, tokens / p.tokens) });
|
|
132
|
+
return q.solOut * solUsd;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const pts = [[0, 0]];
|
|
139
|
+
const pc = await quote(cap);
|
|
140
|
+
if (pc === null)
|
|
141
|
+
return null;
|
|
142
|
+
pts.push([cap, pc]);
|
|
143
|
+
if (owed > 0 && pc > owed) {
|
|
144
|
+
const m = k.price;
|
|
145
|
+
const a = Math.max(0, (1 - pc / (cap * m)) / cap);
|
|
146
|
+
// smaller root of a·m·t² − m·t + owed = 0 (or the no-impact line when a = 0)
|
|
147
|
+
const disc = m * m - 4 * a * m * owed;
|
|
148
|
+
const est = a > 0 && disc > 0 ? (m - Math.sqrt(disc)) / (2 * a * m) : owed / m;
|
|
149
|
+
const t1 = Math.min(cap, Math.max(cap * 1e-6, est));
|
|
150
|
+
const p1 = await quote(t1);
|
|
151
|
+
if (p1 !== null) {
|
|
152
|
+
pts.push([t1, p1]);
|
|
153
|
+
if (p1 < owed && p1 > 0) {
|
|
154
|
+
const t2 = Math.min(cap, (t1 * owed) / p1);
|
|
155
|
+
const p2 = t2 > t1 ? await quote(t2) : null;
|
|
156
|
+
if (p2 !== null)
|
|
157
|
+
pts.push([t2, p2]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
pts.sort((x, y) => x[0] - y[0]);
|
|
162
|
+
for (let i = 1; i < pts.length; i++)
|
|
163
|
+
pts[i][1] = Math.max(pts[i][1], pts[i - 1][1]); // monotone
|
|
164
|
+
const sellUsd = (t) => {
|
|
165
|
+
if (t <= 0)
|
|
166
|
+
return 0;
|
|
167
|
+
for (let i = 1; i < pts.length; i++) {
|
|
168
|
+
const [x0, y0] = pts[i - 1], [x1, y1] = pts[i];
|
|
169
|
+
if (t <= x1)
|
|
170
|
+
return x1 === x0 ? y1 : y0 + ((y1 - y0) * (t - x0)) / (x1 - x0);
|
|
171
|
+
}
|
|
172
|
+
return pts[pts.length - 1][1];
|
|
173
|
+
};
|
|
174
|
+
return { sellUsd, quotes };
|
|
175
|
+
}
|
|
176
|
+
/** The desk's paper book in USD (for `bankroll.source: desk`): cash, open cost, and their sum. */
|
|
177
|
+
book() {
|
|
178
|
+
const solUsd = this.d.solUsd();
|
|
179
|
+
if (!solUsd)
|
|
180
|
+
return null;
|
|
181
|
+
const v = this.d.desk.pnl();
|
|
182
|
+
const cashUsd = v.balanceSol * solUsd;
|
|
183
|
+
const openCostUsd = v.openCostSol * solUsd;
|
|
184
|
+
return { paper: true, cashUsd: r6(cashUsd), openCostUsd: r6(openCostUsd), equityUsd: r6(cashUsd + openCostUsd), solUsd };
|
|
185
|
+
}
|
|
186
|
+
/** The desk's exit plans (read-only). */
|
|
187
|
+
exitPlans() { return this.d.desk.exitPlans(); }
|
|
188
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// dexterm autotrade — the strategy engine (task-1498 spike, AUTOTRADE-SPEC §1, §4, §6).
|
|
2
|
+
//
|
|
3
|
+
// Pure, deterministic, no I/O. Three jobs:
|
|
4
|
+
// screen() does this tick make the coin a candidate under the strategy's entry rules?
|
|
5
|
+
// clampEntry() turn an agent's (or the auto rule's) entry request into the size the strategy allows,
|
|
6
|
+
// with every reduction named. The agent can ask for anything; it gets at most this.
|
|
7
|
+
// SimBook the paper book + the fill model shared by replays and backtests (constant-product impact
|
|
8
|
+
// on the recorded liquidity, fee, slippage, partial fills at the impact cap, unsellable exits).
|
|
9
|
+
//
|
|
10
|
+
// In a live paper run the fills come from the app's paper desk instead (real Jupiter quotes, the mandate
|
|
11
|
+
// caps, the journal). The clamp is the first of
|
|
12
|
+
// three independent fences, never the only one.
|
|
13
|
+
import { parseFilter } from "../../cli/src/screener/filter.js";
|
|
14
|
+
import { tickRow } from "./replay.js";
|
|
15
|
+
export const DAY_MS = 86_400_000;
|
|
16
|
+
const r2 = (x) => Math.round(x * 100) / 100;
|
|
17
|
+
const r6 = (x) => Number(x.toPrecision(6));
|
|
18
|
+
/** Compiled entry rules (the filter is parsed once per strategy). */
|
|
19
|
+
export class EntryRules {
|
|
20
|
+
strategy;
|
|
21
|
+
predicate;
|
|
22
|
+
constructor(strategy) {
|
|
23
|
+
this.strategy = strategy;
|
|
24
|
+
const f = parseFilter(strategy.entry.screen);
|
|
25
|
+
if (!f.ok)
|
|
26
|
+
throw new Error(`entry.screen: ${f.error}`);
|
|
27
|
+
this.predicate = (k) => f.filter.predicate(tickRow(k));
|
|
28
|
+
}
|
|
29
|
+
screen(k) {
|
|
30
|
+
const e = this.strategy.entry;
|
|
31
|
+
const failed = [];
|
|
32
|
+
const matched = [];
|
|
33
|
+
if (k.kind !== 'live')
|
|
34
|
+
failed.push('not-listed');
|
|
35
|
+
if (k.price === null || k.price <= 0)
|
|
36
|
+
failed.push('no-price');
|
|
37
|
+
if (!this.predicate(k))
|
|
38
|
+
failed.push('screen');
|
|
39
|
+
const ageH = k.createdAt ? (k.t - k.createdAt) / 3600e3 : null;
|
|
40
|
+
if (ageH === null)
|
|
41
|
+
failed.push('age-unknown');
|
|
42
|
+
else if (ageH < e.ageHours.min || (e.ageHours.max !== null && ageH > e.ageHours.max))
|
|
43
|
+
failed.push('age');
|
|
44
|
+
const s = e.safety;
|
|
45
|
+
if (s.requireNoMintAuthority && k.mintAuth !== false)
|
|
46
|
+
failed.push(k.mintAuth === null ? 'mint-authority-unknown' : 'mint-authority');
|
|
47
|
+
if (s.requireNoFreezeAuthority && k.freezeAuth !== false)
|
|
48
|
+
failed.push(k.freezeAuth === null ? 'freeze-authority-unknown' : 'freeze-authority');
|
|
49
|
+
if (s.maxTop10Pct !== null) {
|
|
50
|
+
if (k.top10 === null) {
|
|
51
|
+
if (s.unknownHolders === 'reject')
|
|
52
|
+
failed.push('holders-unknown');
|
|
53
|
+
}
|
|
54
|
+
else if (k.top10 > s.maxTop10Pct)
|
|
55
|
+
failed.push('top10');
|
|
56
|
+
}
|
|
57
|
+
if (s.maxDevPct !== null && k.dev !== null && k.dev > s.maxDevPct)
|
|
58
|
+
failed.push('dev-share');
|
|
59
|
+
if (s.maxMcapSplitX !== null && (k.mcapFlagged || splitX(k) > s.maxMcapSplitX))
|
|
60
|
+
failed.push('mcap-split');
|
|
61
|
+
if (s.youngVertical && ageH !== null && ageH < s.youngVertical.maxAgeHours && (k.chg1h ?? 0) >= s.youngVertical.maxH1Pct)
|
|
62
|
+
failed.push('young-vertical');
|
|
63
|
+
const fired = e.triggers.map((t) => {
|
|
64
|
+
switch (t.kind) {
|
|
65
|
+
case 'screen': return true;
|
|
66
|
+
case 'momentum': return (k.chg5m ?? -Infinity) >= t.m5MinPct && ratio(k.buys5m, k.sells5m) >= t.buySellMin;
|
|
67
|
+
// Live feeds only: a replay has no narrative heat or followed-wallet stream, so these never fire here.
|
|
68
|
+
case 'narrative': return false;
|
|
69
|
+
case 'wallet': return false;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
e.triggers.forEach((t, i) => { if (fired[i])
|
|
73
|
+
matched.push(`trigger:${t.kind}`); });
|
|
74
|
+
const trig = e.triggerMode === 'all' ? fired.every(Boolean) : fired.some(Boolean);
|
|
75
|
+
if (!trig)
|
|
76
|
+
failed.push('trigger');
|
|
77
|
+
return { pass: failed.length === 0, matched, failed };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function splitX(k) {
|
|
81
|
+
if (!k.mcap || !k.mcapResolved)
|
|
82
|
+
return 1;
|
|
83
|
+
return Math.max(k.mcap, k.mcapResolved) / Math.max(1e-9, Math.min(k.mcap, k.mcapResolved));
|
|
84
|
+
}
|
|
85
|
+
const ratio = (a, b) => (a === null || b === null ? 0 : b === 0 ? (a > 0 ? Infinity : 0) : a / b);
|
|
86
|
+
/** Buy `usd` against the recorded pool. Constant product on half the liquidity; a partial fill at the impact cap. */
|
|
87
|
+
export function simBuy(k, usd, x) {
|
|
88
|
+
if (!k.price || k.price <= 0)
|
|
89
|
+
return { refused: 'no-price' };
|
|
90
|
+
if (!k.liq || k.liq <= 0)
|
|
91
|
+
return { refused: 'no-liquidity' };
|
|
92
|
+
const R = k.liq / 2;
|
|
93
|
+
const net = usd - x.networkFeeUsd;
|
|
94
|
+
if (net <= 0)
|
|
95
|
+
return { refused: 'below-network-fee' };
|
|
96
|
+
let input = net * (1 - x.feeBps / 1e4);
|
|
97
|
+
// Constant product: the average price paid is mid × (1 + input / R), so the impact is input / R.
|
|
98
|
+
const capIn = R * (x.maxPriceImpactPct / 100);
|
|
99
|
+
const partial = input > capIn;
|
|
100
|
+
if (partial)
|
|
101
|
+
input = capIn;
|
|
102
|
+
const tokens = ((R / k.price) * input) / (R + input) / (1 + x.slippageBps / 1e4);
|
|
103
|
+
const spent = partial ? input / (1 - x.feeBps / 1e4) + x.networkFeeUsd : usd;
|
|
104
|
+
return { t: k.t, side: 'buy', usd: r6(spent), tokens, price: k.price, avgPrice: r6(spent / tokens), impactPct: r2((input / R) * 100), partial, reason: 'entry' };
|
|
105
|
+
}
|
|
106
|
+
/** Sell `tokens` into the recorded pool. Below minExitLiquidityUsd (or no pool) nothing sells: the bag is stuck. */
|
|
107
|
+
export function simSell(k, tokens, x, reason) {
|
|
108
|
+
if (!k.price || k.price <= 0)
|
|
109
|
+
return { refused: 'no-price' };
|
|
110
|
+
if (!k.liq || k.liq < x.minExitLiquidityUsd)
|
|
111
|
+
return { refused: 'illiquid' };
|
|
112
|
+
const R = k.liq / 2;
|
|
113
|
+
const V = tokens * k.price;
|
|
114
|
+
const gross = (V * R) / (R + V);
|
|
115
|
+
const usd = gross * (1 - x.feeBps / 1e4) * (1 - x.slippageBps / 1e4) - x.networkFeeUsd;
|
|
116
|
+
if (usd <= 0)
|
|
117
|
+
return { refused: 'dust' };
|
|
118
|
+
return { t: k.t, side: 'sell', usd: r6(usd), tokens, price: k.price, avgPrice: r6(usd / tokens), impactPct: r2((V / (R + V)) * 100), partial: false, reason };
|
|
119
|
+
}
|
|
120
|
+
/** Value of a position if sold now through the same model (0 when it cannot be sold). */
|
|
121
|
+
export function liquidationValue(p, k, x) {
|
|
122
|
+
if (p.tokens <= 0)
|
|
123
|
+
return 0;
|
|
124
|
+
const f = simSell(k, p.tokens, x, 'mark');
|
|
125
|
+
return 'refused' in f ? 0 : f.usd;
|
|
126
|
+
}
|
|
127
|
+
export class SimBook {
|
|
128
|
+
strategy;
|
|
129
|
+
state;
|
|
130
|
+
open = new Map();
|
|
131
|
+
closed = [];
|
|
132
|
+
seq = 0;
|
|
133
|
+
triggers = new Map();
|
|
134
|
+
/** The shared paper account this book trades from (task-1538), or null: the book owns its bankroll. */
|
|
135
|
+
account;
|
|
136
|
+
constructor(strategy, now, account, member) {
|
|
137
|
+
this.strategy = strategy;
|
|
138
|
+
this.account = account ?? null;
|
|
139
|
+
const start = account ? account.bookEquity() : strategy.bankroll.startUsd;
|
|
140
|
+
this.state = { cashUsd: start, peakEquityUsd: start, dayStart: dayOf(now), dayStartEquityUsd: start, dayRealizedUsd: 0, entriesToday: 0, lossStreak: 0, cooldownUntil: 0, halted: null, sizeDown: false, perCoin: {} };
|
|
141
|
+
if (account) {
|
|
142
|
+
// On a shared account the cash IS the account's: every fill and sale below moves the wallet's cash.
|
|
143
|
+
Object.defineProperty(this.state, 'cashUsd', { get: () => account.cashUsd, set: (v) => { account.cashUsd = v; }, enumerable: true });
|
|
144
|
+
account.join(this, member);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Book equity: cash + the cost basis still open (unrealized gains do not count for sizing). On an account: the account's. */
|
|
148
|
+
bookEquity() {
|
|
149
|
+
if (this.account)
|
|
150
|
+
return this.account.bookEquity();
|
|
151
|
+
let e = this.state.cashUsd;
|
|
152
|
+
for (const p of this.open.values())
|
|
153
|
+
e += Math.max(0, p.costUsd - p.realizedUsd);
|
|
154
|
+
return e;
|
|
155
|
+
}
|
|
156
|
+
/** Marked equity: cash + what every open bag would sell for now (stuck bags count 0). On an account: the account's. */
|
|
157
|
+
markedEquity() {
|
|
158
|
+
if (this.account)
|
|
159
|
+
return this.account.markedEquity();
|
|
160
|
+
let e = this.state.cashUsd;
|
|
161
|
+
for (const p of this.open.values())
|
|
162
|
+
e += liquidationValue(p, p.lastTick, this.strategy.execution);
|
|
163
|
+
return e;
|
|
164
|
+
}
|
|
165
|
+
/** This book's own open cost basis (its exposure, also on a shared account). */
|
|
166
|
+
exposureUsd() { return this.openCostUsd(); }
|
|
167
|
+
openCostUsd() {
|
|
168
|
+
let e = 0;
|
|
169
|
+
for (const p of this.open.values())
|
|
170
|
+
e += Math.max(0, p.costUsd - p.realizedUsd);
|
|
171
|
+
return e;
|
|
172
|
+
}
|
|
173
|
+
/** What this book's open bags would sell for now. */
|
|
174
|
+
openMarkUsd() {
|
|
175
|
+
let e = 0;
|
|
176
|
+
for (const p of this.open.values())
|
|
177
|
+
e += liquidationValue(p, p.lastTick, this.strategy.execution);
|
|
178
|
+
return e;
|
|
179
|
+
}
|
|
180
|
+
/** This book's own P/L: closed trades + the open bags marked now (on a shared account, the strategy's share). */
|
|
181
|
+
ownPnlUsd() {
|
|
182
|
+
let e = 0;
|
|
183
|
+
for (const t of this.closed)
|
|
184
|
+
e += t.pnlUsd;
|
|
185
|
+
for (const p of this.open.values())
|
|
186
|
+
e += liquidationValue(p, p.lastTick, this.strategy.execution) + p.realizedUsd - p.costUsd;
|
|
187
|
+
return e;
|
|
188
|
+
}
|
|
189
|
+
/** Day roll, drawdown size-down / halt. Called by the runner on every step. */
|
|
190
|
+
roll(now) {
|
|
191
|
+
const s = this.state;
|
|
192
|
+
const d = dayOf(now);
|
|
193
|
+
if (d !== s.dayStart) {
|
|
194
|
+
s.dayStart = d;
|
|
195
|
+
s.dayRealizedUsd = 0;
|
|
196
|
+
s.entriesToday = 0;
|
|
197
|
+
s.dayStartEquityUsd = this.bookEquity();
|
|
198
|
+
if (s.halted === 'daily-loss')
|
|
199
|
+
s.halted = null;
|
|
200
|
+
}
|
|
201
|
+
const eq = this.markedEquity();
|
|
202
|
+
s.peakEquityUsd = Math.max(s.peakEquityUsd, eq);
|
|
203
|
+
const dd = ((s.peakEquityUsd - eq) / s.peakEquityUsd) * 100;
|
|
204
|
+
const r = this.strategy.risk.drawdown;
|
|
205
|
+
if (dd >= r.haltAtPct && !s.halted)
|
|
206
|
+
s.halted = 'drawdown';
|
|
207
|
+
if (dd >= r.sizeDownAtPct)
|
|
208
|
+
s.sizeDown = true;
|
|
209
|
+
else if (dd < r.recoverBelowPct)
|
|
210
|
+
s.sizeDown = false;
|
|
211
|
+
}
|
|
212
|
+
openPosition(k, fill, profile, sessionId, trigger, ladder) {
|
|
213
|
+
const id = `p${++this.seq}`;
|
|
214
|
+
const p = {
|
|
215
|
+
id, mint: k.mint, symbol: k.symbol, profileId: profile.id, openedAt: k.t, sessionId,
|
|
216
|
+
tokens: fill.tokens, origTokens: fill.tokens, costUsd: fill.usd, realizedUsd: 0, entryPrice: fill.price, peakPrice: fill.price, troughPrice: fill.price,
|
|
217
|
+
tpFilled: 0, trailArmed: false, ladder, fills: [fill], status: 'open', stuck: false, closedAt: null, exitReason: null,
|
|
218
|
+
liqWindow: [{ t: k.t, liq: k.liq ?? 0 }], lowTapeSince: null, lastTick: k, wakeSent: {},
|
|
219
|
+
};
|
|
220
|
+
this.state.cashUsd -= fill.usd;
|
|
221
|
+
this.state.entriesToday++;
|
|
222
|
+
const c = (this.state.perCoin[k.mint] ??= { entries: 0, lastExit: 0 });
|
|
223
|
+
c.entries++;
|
|
224
|
+
this.open.set(k.mint, p);
|
|
225
|
+
this.triggers.set(id, trigger);
|
|
226
|
+
return p;
|
|
227
|
+
}
|
|
228
|
+
addTranche(p, fill) {
|
|
229
|
+
this.state.cashUsd -= fill.usd;
|
|
230
|
+
const tokens = p.tokens + fill.tokens;
|
|
231
|
+
p.entryPrice = (p.entryPrice * p.tokens + fill.price * fill.tokens) / tokens;
|
|
232
|
+
p.tokens = tokens;
|
|
233
|
+
p.origTokens += fill.tokens;
|
|
234
|
+
p.costUsd += fill.usd;
|
|
235
|
+
p.fills.push(fill);
|
|
236
|
+
}
|
|
237
|
+
/** Book a sell; closes the position when nothing is left. */
|
|
238
|
+
sell(p, fill) {
|
|
239
|
+
p.tokens = Math.max(0, p.tokens - fill.tokens);
|
|
240
|
+
if (p.tokens < p.origTokens * 1e-6)
|
|
241
|
+
p.tokens = 0;
|
|
242
|
+
p.realizedUsd += fill.usd;
|
|
243
|
+
this.state.cashUsd += fill.usd;
|
|
244
|
+
p.fills.push(fill);
|
|
245
|
+
if (p.tokens > 0)
|
|
246
|
+
return null;
|
|
247
|
+
return this.close(p, fill.t, fill.reason, false);
|
|
248
|
+
}
|
|
249
|
+
/** Close the books on a position (also used at the end of a run: stuck bags close at 0). */
|
|
250
|
+
close(p, t, reason, stuck) {
|
|
251
|
+
p.status = 'closed';
|
|
252
|
+
p.closedAt = t;
|
|
253
|
+
p.exitReason = reason;
|
|
254
|
+
p.stuck = stuck;
|
|
255
|
+
this.open.delete(p.mint);
|
|
256
|
+
const pnl = p.realizedUsd - p.costUsd;
|
|
257
|
+
const s = this.state;
|
|
258
|
+
s.dayRealizedUsd += pnl;
|
|
259
|
+
if (pnl < 0) {
|
|
260
|
+
s.lossStreak++;
|
|
261
|
+
if (s.lossStreak >= this.strategy.risk.lossStreak.losses) {
|
|
262
|
+
s.cooldownUntil = t + this.strategy.risk.lossStreak.cooldownMin * 60e3;
|
|
263
|
+
s.lossStreak = 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
s.lossStreak = 0;
|
|
268
|
+
const lossPct = (-s.dayRealizedUsd / s.dayStartEquityUsd) * 100;
|
|
269
|
+
if (lossPct >= this.strategy.risk.dailyLossHaltPct && !s.halted)
|
|
270
|
+
s.halted = 'daily-loss';
|
|
271
|
+
(s.perCoin[p.mint] ??= { entries: 0, lastExit: 0 }).lastExit = t;
|
|
272
|
+
const ct = {
|
|
273
|
+
id: p.id, mint: p.mint, symbol: p.symbol, profileId: p.profileId, openedAt: p.openedAt, closedAt: t,
|
|
274
|
+
costUsd: r6(p.costUsd), proceedsUsd: r6(p.realizedUsd), pnlUsd: r6(pnl), pnlPct: r2((pnl / p.costUsd) * 100),
|
|
275
|
+
exitReason: reason, stuck, entryTrigger: this.triggers.get(p.id) ?? [], sessionId: p.sessionId, fills: p.fills,
|
|
276
|
+
};
|
|
277
|
+
this.closed.push(ct);
|
|
278
|
+
return ct;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export const dayOf = (t) => Math.floor(t / DAY_MS);
|
|
282
|
+
/**
|
|
283
|
+
* The strategy's answer to "may I enter, and with how much?". Order: refusals (kill switch, halts,
|
|
284
|
+
* cooldowns, counts), then the size the strategy itself picks, then every ceiling, each reduction recorded.
|
|
285
|
+
* An agent request can only LOWER the size below the strategy's own pick, never raise it above a ceiling.
|
|
286
|
+
*/
|
|
287
|
+
export function clampEntry(book, k, req, killSwitch = false) {
|
|
288
|
+
const s = book.strategy;
|
|
289
|
+
const st = book.state;
|
|
290
|
+
const clamps = [];
|
|
291
|
+
const no = (refused) => ({ ok: false, refused, clamps });
|
|
292
|
+
// task-1571: paper only (dexterm holds no key and places no live order).
|
|
293
|
+
if (s.mode !== 'paper')
|
|
294
|
+
return no('mode-not-paper');
|
|
295
|
+
if (killSwitch)
|
|
296
|
+
return no('kill-switch');
|
|
297
|
+
if (st.halted)
|
|
298
|
+
return no(`halted:${st.halted}`);
|
|
299
|
+
if (k.t < st.cooldownUntil)
|
|
300
|
+
return no('loss-streak-cooldown');
|
|
301
|
+
if (book.open.has(k.mint))
|
|
302
|
+
return no('already-open');
|
|
303
|
+
// Free bags (cost recovered) can stop counting as open positions, up to their own cap (exit profile 1.2.0).
|
|
304
|
+
const rb = s.risk.recoveredBags;
|
|
305
|
+
const bags = rb && !rb.countAsOpen ? [...book.open.values()].filter((p) => p.costRecovered).length : 0;
|
|
306
|
+
if (book.open.size - bags >= s.risk.maxConcurrent)
|
|
307
|
+
return no('max-concurrent');
|
|
308
|
+
if (rb && !rb.countAsOpen && book.open.size >= s.risk.maxConcurrent + rb.maxBags)
|
|
309
|
+
return no('max-open-with-bags');
|
|
310
|
+
if (st.entriesToday >= s.risk.maxEntriesPerDay)
|
|
311
|
+
return no('max-entries-per-day');
|
|
312
|
+
const coin = st.perCoin[k.mint];
|
|
313
|
+
const profileId = req.profile ?? s.exits.default;
|
|
314
|
+
const profile = s.exits.profiles[profileId];
|
|
315
|
+
if (!profile)
|
|
316
|
+
return no(`unknown-profile:${profileId}`);
|
|
317
|
+
if (coin) {
|
|
318
|
+
if (coin.entries >= s.risk.perCoin.maxEntries)
|
|
319
|
+
return no('per-coin-max-entries');
|
|
320
|
+
if (!profile.reentry.allowed)
|
|
321
|
+
return no('profile-no-reentry');
|
|
322
|
+
const cool = Math.max(s.risk.perCoin.cooldownMin, profile.reentry.cooldownMinutes) * 60e3;
|
|
323
|
+
if (k.t - coin.lastExit < cool)
|
|
324
|
+
return no('per-coin-cooldown');
|
|
325
|
+
}
|
|
326
|
+
if ((k.liq ?? 0) < profile.fit.minLiquidityUsd)
|
|
327
|
+
return no('profile-min-liquidity');
|
|
328
|
+
const bank = book.bookEquity();
|
|
329
|
+
const z = s.sizing;
|
|
330
|
+
let usd;
|
|
331
|
+
let basis;
|
|
332
|
+
switch (z.mode) {
|
|
333
|
+
case 'fixed':
|
|
334
|
+
usd = z.fixedUsd ?? 0;
|
|
335
|
+
basis = `fixed $${z.fixedUsd}`;
|
|
336
|
+
break;
|
|
337
|
+
case 'pct':
|
|
338
|
+
usd = (bank * (z.pct ?? 0)) / 100;
|
|
339
|
+
basis = `${z.pct}% of $${r2(bank)}`;
|
|
340
|
+
break;
|
|
341
|
+
case 'risk': {
|
|
342
|
+
const stop = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
|
|
343
|
+
usd = (bank * (z.riskPct ?? 0)) / stop;
|
|
344
|
+
basis = `${z.riskPct}% risk / ${stop}% stop of $${r2(bank)}`;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case 'confidence': {
|
|
348
|
+
const c = z.confidence ?? { minPct: 1, maxPct: 1, default: 0.5 };
|
|
349
|
+
const conf = clamp01(req.confidence ?? c.default);
|
|
350
|
+
const pct = c.minPct + (c.maxPct - c.minPct) * conf;
|
|
351
|
+
usd = (bank * pct) / 100;
|
|
352
|
+
basis = `confidence ${r2(conf)} → ${r2(pct)}% of $${r2(bank)}`;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// The agent may ask for LESS than the strategy's pick (never more).
|
|
357
|
+
const asked = req.sizeUsd ?? (req.sizePct !== undefined ? (bank * req.sizePct) / 100 : undefined);
|
|
358
|
+
if (asked !== undefined && Number.isFinite(asked)) {
|
|
359
|
+
if (asked < usd) {
|
|
360
|
+
basis += `, agent asked $${r2(asked)}`;
|
|
361
|
+
usd = Math.max(0, asked);
|
|
362
|
+
}
|
|
363
|
+
else if (asked > usd)
|
|
364
|
+
clamps.push({ rule: 'strategy-size', fromUsd: r2(asked), toUsd: r2(usd) });
|
|
365
|
+
}
|
|
366
|
+
const cap = (rule, limit) => { if (usd > limit + 1e-9) {
|
|
367
|
+
clamps.push({ rule, fromUsd: r2(usd), toUsd: r2(Math.max(0, limit)) });
|
|
368
|
+
usd = Math.max(0, limit);
|
|
369
|
+
} };
|
|
370
|
+
if (st.sizeDown)
|
|
371
|
+
cap('drawdown-size-down', usd * s.risk.drawdown.sizeDownMult);
|
|
372
|
+
cap('strategy-max-pct', (bank * s.sizing.maxPctOfBankroll) / 100);
|
|
373
|
+
cap('profile-max-pct', (bank * profile.sizing.maxSizePctOfBankroll) / 100);
|
|
374
|
+
cap('strategy-max-usd', s.sizing.maxUsd);
|
|
375
|
+
const lossPct = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
|
|
376
|
+
cap('per-trade-max-loss', (bank * s.risk.perTrade.maxLossPctOfBankroll) / lossPct);
|
|
377
|
+
cap('max-exposure', (bank * s.risk.maxExposurePct) / 100 - book.exposureUsd());
|
|
378
|
+
cap('cash', st.cashUsd);
|
|
379
|
+
const R = (k.liq ?? 0) / 2;
|
|
380
|
+
const imp = s.execution.maxPriceImpactPct / 100;
|
|
381
|
+
cap('price-impact', (R * imp) / (1 - s.execution.feeBps / 1e4) + s.execution.networkFeeUsd);
|
|
382
|
+
if (usd < s.sizing.minUsd)
|
|
383
|
+
return no(clamps.length ? `below-min-after-${clamps[clamps.length - 1].rule}` : 'below-min-size');
|
|
384
|
+
const first = z.ladder ? usd * z.ladder[0].frac : usd;
|
|
385
|
+
return { ok: true, usd: r2(usd), firstTrancheUsd: r2(first), profile, clamps, basis };
|
|
386
|
+
}
|
|
387
|
+
const clamp01 = (x) => (Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : 0.5);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// dexterm autotrade — the execution port (task-1527, AUTOTRADE-SPEC §8.1, §8.5 step 7). PAPER today.
|
|
2
|
+
//
|
|
3
|
+
// Where an approved entry goes. The runner's Executor (sim book, paper desk) is the paper side; a live port (the
|
|
4
|
+
// local signer with its caps, a later card) is the other side, behind the same three calls:
|
|
5
|
+
//
|
|
6
|
+
// entryBlock() why no entry can go through right now (null = ready). A live port says "live is off", "the
|
|
7
|
+
// wallet is locked", "the kill switch is engaged", "no caps file" here.
|
|
8
|
+
// confirmRemote() a chat button approved this exact order. The port cannot check the chat itself (it holds no
|
|
9
|
+
// bot token), so it gets the connector's evidence {proposalHash, senderId, chatId, updateId} and
|
|
10
|
+
// answers with a SINGLE-USE proof bound to the order's hash, or a refusal. A live port checks its
|
|
11
|
+
// own caps here (per trade at the remote-button tier, its remote daily cap, the allowed
|
|
12
|
+
// strategies, slippage and impact) and refuses anything outside them, whatever the broker said.
|
|
13
|
+
// buy() / sell() the Executor calls; an approved buy carries its proof in ctx and a port that issued proofs
|
|
14
|
+
// refuses a buy whose proof is missing, spent, expired or bound to another order (the proof is
|
|
15
|
+
// bound to the order's hash and the proposal id, and needs a 64-hex proposal hash).
|
|
16
|
+
//
|
|
17
|
+
// The port has no `badge` field: the runner's Executor may carry its own optional order-line badge (DEVNET/LIVE, the
|
|
18
|
+
// live signer's executor), and PAPER must never print there. Chat derives its badge from `network` (badgeOf).
|
|
19
|
+
//
|
|
20
|
+
// The paper port issues its proofs locally (there is nothing to sign). The live stub refuses everything: it exists
|
|
21
|
+
// so the approval path, the runner and the docs can name the live side before it is installed, and so a
|
|
22
|
+
// misconfiguration fails closed. Nothing in this file holds a key, signs, or touches the network.
|
|
23
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
24
|
+
import { canonical } from "./strategy.js";
|
|
25
|
+
export const badgeOf = (n) => (n === 'paper' ? 'PAPER' : n === 'devnet' ? 'DEVNET' : 'LIVE');
|
|
26
|
+
/** sha256 (hex, 32 chars) of the canonical order: the proof binds to it (the runner hands it back in OrderContext). */
|
|
27
|
+
export const orderHash = (o) => createHash('sha256').update(canonical(o)).digest('hex').slice(0, 32);
|
|
28
|
+
export const PROOF_TTL_MS = 30_000;
|
|
29
|
+
const PROPOSAL_HASH = /^[0-9a-f]{64}$/;
|
|
30
|
+
/**
|
|
31
|
+
* The paper side: the runner's Executor behind the port calls. confirmRemote issues a local single-use proof
|
|
32
|
+
* (PROOF_TTL_MS) for the order; a buy that carries a proofId must match it (order hash, proposal id, mint, size
|
|
33
|
+
* at most the approved one) and spends it. A buy
|
|
34
|
+
* without one is an ordinary runner entry (no approval was required) and goes straight to the executor.
|
|
35
|
+
*/
|
|
36
|
+
export class PaperPort {
|
|
37
|
+
network = 'paper';
|
|
38
|
+
name;
|
|
39
|
+
exec;
|
|
40
|
+
proofs = new Map();
|
|
41
|
+
now;
|
|
42
|
+
block;
|
|
43
|
+
constructor(exec, o = {}) {
|
|
44
|
+
this.exec = exec;
|
|
45
|
+
this.name = `paper-port(${exec.name})`;
|
|
46
|
+
this.now = o.now ?? Date.now;
|
|
47
|
+
this.block = o.entryBlock ?? (() => null);
|
|
48
|
+
}
|
|
49
|
+
entryBlock() { return this.block(); }
|
|
50
|
+
async confirmRemote(order, ev) {
|
|
51
|
+
if (order.network !== 'paper')
|
|
52
|
+
return { ok: false, refused: 'port-network-mismatch' };
|
|
53
|
+
if (ev.tier !== 'remote-button')
|
|
54
|
+
return { ok: false, refused: 'tier-unknown' };
|
|
55
|
+
if (!PROPOSAL_HASH.test(ev.proposalHash) || !ev.proposalId)
|
|
56
|
+
return { ok: false, refused: 'evidence-malformed' };
|
|
57
|
+
const b = this.entryBlock();
|
|
58
|
+
if (b)
|
|
59
|
+
return { ok: false, refused: b };
|
|
60
|
+
if (!(order.usd > 0))
|
|
61
|
+
return { ok: false, refused: 'size-not-positive' };
|
|
62
|
+
const proofId = randomBytes(12).toString('base64url');
|
|
63
|
+
const expiresAt = this.now() + PROOF_TTL_MS;
|
|
64
|
+
this.proofs.set(proofId, { orderHash: orderHash(order), proposalId: ev.proposalId, usd: order.usd, mint: order.mint, expiresAt });
|
|
65
|
+
return { ok: true, proofId, expiresAt };
|
|
66
|
+
}
|
|
67
|
+
async buy(k, usd, ctx) {
|
|
68
|
+
if (ctx?.proofId !== undefined) {
|
|
69
|
+
const p = this.proofs.get(ctx.proofId);
|
|
70
|
+
this.proofs.delete(ctx.proofId); // single use, whatever happens next
|
|
71
|
+
if (!p)
|
|
72
|
+
return { refused: 'proof-unknown-or-spent' };
|
|
73
|
+
if (this.now() > p.expiresAt)
|
|
74
|
+
return { refused: 'proof-expired' };
|
|
75
|
+
if (ctx.orderHash !== p.orderHash || ctx.proposalId !== p.proposalId || p.mint !== k.mint || usd > p.usd + 1e-9)
|
|
76
|
+
return { refused: 'proof-order-mismatch' };
|
|
77
|
+
}
|
|
78
|
+
return this.exec.buy(k, usd, ctx);
|
|
79
|
+
}
|
|
80
|
+
sell(k, tokens, reason) {
|
|
81
|
+
return this.exec.sell(k, tokens, reason);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The live side before it is installed: every call refuses. A runner or a broker that is handed this port takes no
|
|
86
|
+
* entry and says why; exits that would need it are refused too (a live position cannot exist without a live port).
|
|
87
|
+
*/
|
|
88
|
+
export class LivePortStub {
|
|
89
|
+
network;
|
|
90
|
+
name = 'live-port-stub';
|
|
91
|
+
static REASON = 'live-port-not-installed';
|
|
92
|
+
constructor(network = 'devnet') { this.network = network; }
|
|
93
|
+
entryBlock() { return LivePortStub.REASON; }
|
|
94
|
+
async confirmRemote() { return { ok: false, refused: LivePortStub.REASON }; }
|
|
95
|
+
buy() { return { refused: LivePortStub.REASON }; }
|
|
96
|
+
sell() { return { refused: LivePortStub.REASON }; }
|
|
97
|
+
}
|