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,190 @@
|
|
|
1
|
+
// dexterm autotrade — the position monitor (task-1498 spike, AUTOTRADE-SPEC §5).
|
|
2
|
+
//
|
|
3
|
+
// No LLM in this loop. On every tick of a held coin it applies the exit profile's mechanical rules at once
|
|
4
|
+
// (rug / liquidity pull, thesis breaks, hard stop, take-profit ladder, trailing stop, time stop, ladder adds)
|
|
5
|
+
// and only then raises WAKE events for the judgement calls the profile hands to the position's agent session
|
|
6
|
+
// (a TP filled, a time review, a drawdown alert, an early thesis warning, a mechanical exit that already
|
|
7
|
+
// happened). The session can answer within bounds (hold / exit / tighten / re-profile); it can never block
|
|
8
|
+
// or undo a mechanical exit, and if it does not answer the mechanical rules simply keep running.
|
|
9
|
+
import { splitX } from "./engine.js";
|
|
10
|
+
export const pnlPct = (p, price) => ((price - p.entryPrice) / p.entryPrice) * 100;
|
|
11
|
+
/** The data layer's agreement ratio (@dexterm/data MCAP_AGREE_RATIO; equal by test, kept local so the engine stays pure). */
|
|
12
|
+
export const SOURCES_AGREE_X = 5;
|
|
13
|
+
/**
|
|
14
|
+
* Why a thesis-split fired, naming the trigger that did fire (task-1518). On a row the data layer flagged, the resolved
|
|
15
|
+
* cap IS DexScreener's (resolveMcap keeps it when the sources disagree), so the two numbers on the tick are equal and
|
|
16
|
+
* the computed ratio reads 1.0x: say that the sources' supplies were flagged instead of printing "1.0x apart".
|
|
17
|
+
*/
|
|
18
|
+
export function splitDetail(k, maxX) {
|
|
19
|
+
const x = splitX(k);
|
|
20
|
+
if (x > maxX)
|
|
21
|
+
return `market-cap sources ${x.toFixed(1)}x apart (limit ${maxX}x)`;
|
|
22
|
+
return `the data layer flagged the two market-cap sources: their supplies are more than ${SOURCES_AGREE_X}x apart`;
|
|
23
|
+
}
|
|
24
|
+
const pct = (x) => `${x >= 0 ? '+' : ''}${x.toFixed(1)}%`;
|
|
25
|
+
/**
|
|
26
|
+
* One tick for one open position. Mutates the position's tracking fields (peak, windows, TP count, armed
|
|
27
|
+
* trail, wake flags) and returns the actions in the order they must be executed. A full exit ends the list.
|
|
28
|
+
*/
|
|
29
|
+
export function monitorTick(p, prof, k, o) {
|
|
30
|
+
const out = [];
|
|
31
|
+
p.lastTick = k;
|
|
32
|
+
const wake = (event, detail, allowed = ['hold', 'exit', 'tighten']) => {
|
|
33
|
+
out.push({ kind: 'wake', wake: { event, positionId: p.id, mint: p.mint, t: k.t, detail, allowed } });
|
|
34
|
+
};
|
|
35
|
+
const exitAll = (reason, detail) => {
|
|
36
|
+
out.push({ kind: 'sell', tokens: p.tokens, all: true, reason, detail });
|
|
37
|
+
out.push({ kind: 'wake', wake: { event: 'mechanical_exit', positionId: p.id, mint: p.mint, t: k.t, detail: `${reason}: ${detail}`, allowed: ['hold'] } });
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
// A stuck bag (no pool to sell into) is retried on every tick; nothing else applies until it sells.
|
|
41
|
+
if (p.stuck) {
|
|
42
|
+
if ((k.liq ?? 0) >= o.minExitLiquidityUsd && k.price)
|
|
43
|
+
return exitAll('rug-liquidity', 'liquidity came back, selling the stuck bag');
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
if (k.price === null || k.price <= 0)
|
|
47
|
+
return out;
|
|
48
|
+
const liq = k.liq ?? 0;
|
|
49
|
+
// 1. Rug / liquidity pull: the pool is gone or below the exit floor.
|
|
50
|
+
if (liq < o.minExitLiquidityUsd)
|
|
51
|
+
return exitAll('rug-liquidity', `liquidity $${Math.round(liq)} is below the $${o.minExitLiquidityUsd} exit floor`);
|
|
52
|
+
// 2. Thesis breaks on the coin's own reads.
|
|
53
|
+
const tb = prof.thesisBreak;
|
|
54
|
+
const winMs = tb.liquidityWindowMin * 60e3;
|
|
55
|
+
p.liqWindow.push({ t: k.t, liq });
|
|
56
|
+
// Keep the window plus the newest read before it: on a ~2-minute tape a 5-minute window still has a reference.
|
|
57
|
+
while (p.liqWindow.length > 2 && p.liqWindow[1].t < k.t - winMs)
|
|
58
|
+
p.liqWindow.shift();
|
|
59
|
+
const liqHigh = Math.max(...p.liqWindow.map((w) => w.liq));
|
|
60
|
+
const liqDrop = liqHigh > 0 ? ((liqHigh - liq) / liqHigh) * 100 : 0;
|
|
61
|
+
if (liqDrop >= tb.liquidityDropPct)
|
|
62
|
+
return exitAll('thesis-liquidity', `liquidity down ${liqDrop.toFixed(0)}% from its ${tb.liquidityWindowMin}-min high`);
|
|
63
|
+
if (tb.mcapDivergenceX !== null && (k.mcapFlagged || splitX(k) > tb.mcapDivergenceX))
|
|
64
|
+
return exitAll('thesis-split', splitDetail(k, tb.mcapDivergenceX));
|
|
65
|
+
if (tb.volumeDeath) {
|
|
66
|
+
const trades = (k.buys5m ?? 0) + (k.sells5m ?? 0);
|
|
67
|
+
if (k.buys5m !== null && trades < tb.volumeDeath.m5TradesBelow)
|
|
68
|
+
p.lowTapeSince ??= k.t;
|
|
69
|
+
else
|
|
70
|
+
p.lowTapeSince = null;
|
|
71
|
+
if (p.lowTapeSince !== null && k.t - p.lowTapeSince >= tb.volumeDeath.forMinutes * 60e3)
|
|
72
|
+
return exitAll('thesis-volume', `under ${tb.volumeDeath.m5TradesBelow} trades per 5 min for ${tb.volumeDeath.forMinutes} min`);
|
|
73
|
+
}
|
|
74
|
+
// (devSell needs chain events; the recorded history has none, so a replay cannot fire it.)
|
|
75
|
+
const gain = pnlPct(p, k.price);
|
|
76
|
+
p.peakPrice = Math.max(p.peakPrice, k.price);
|
|
77
|
+
p.troughPrice = Math.min(p.troughPrice ?? p.entryPrice, k.price);
|
|
78
|
+
const peakGain = pnlPct(p, p.peakPrice);
|
|
79
|
+
// 3. Hard stop (opt-in per profile), then the holding cap (the position's own dead-man).
|
|
80
|
+
if (prof.maxHoldMinutes != null && k.t - p.openedAt >= prof.maxHoldMinutes * 60e3)
|
|
81
|
+
return exitAll('max-hold', `held ${prof.maxHoldMinutes} min (the profile's holding cap) at ${pct(gain)}`);
|
|
82
|
+
if (prof.hardStop.enabled && prof.hardStop.atPct !== null && gain <= prof.hardStop.atPct)
|
|
83
|
+
return exitAll('stop', `${pct(gain)} hit the ${prof.hardStop.atPct}% hard stop`);
|
|
84
|
+
// 3b. Cost recovery (exit profile 1.2.0): sell what pays back the net cost, once; the rest rides as a free bag.
|
|
85
|
+
// Proceeds come from the runner's own fill model (every cost a sell pays), and the sell is capped at
|
|
86
|
+
// maxSellFrac of the bag, so a thin pool recovers less rather than selling everything.
|
|
87
|
+
const cr = prof.costRecovery;
|
|
88
|
+
if (cr && !p.costRecovered && gain >= cr.atPct) {
|
|
89
|
+
const owed = p.costUsd - p.realizedUsd;
|
|
90
|
+
const price = k.price;
|
|
91
|
+
const sellUsd = o.sellUsd ?? ((tok) => tok * price);
|
|
92
|
+
const cap = p.tokens * cr.maxSellFrac;
|
|
93
|
+
let tokens = cap;
|
|
94
|
+
if (sellUsd(cap) > owed) { // smallest bag slice that nets the cost (proceeds rise with size)
|
|
95
|
+
let lo = 0, hi = cap;
|
|
96
|
+
for (let i = 0; i < 40; i++) {
|
|
97
|
+
const mid = (lo + hi) / 2;
|
|
98
|
+
if (sellUsd(mid) >= owed)
|
|
99
|
+
hi = mid;
|
|
100
|
+
else
|
|
101
|
+
lo = mid;
|
|
102
|
+
}
|
|
103
|
+
tokens = hi;
|
|
104
|
+
}
|
|
105
|
+
p.costRecovered = true;
|
|
106
|
+
if (owed > 0 && tokens > 0) {
|
|
107
|
+
out.push({ kind: 'sell', tokens, all: false, reason: 'cost-recovery', detail: `${pct(gain)} ≥ +${cr.atPct}%: sell ${Math.round((tokens / p.tokens) * 100)}% of the bag to take the $${owed.toFixed(2)} cost back` });
|
|
108
|
+
if (prof.wake?.onCostRecovered)
|
|
109
|
+
wake('cost_recovered', `cost taken back at ${pct(gain)}; ${Math.round(((p.tokens - tokens) / p.origTokens) * 100)}% of the original size rides as a free bag`, ['hold', 'exit', 'tighten']);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const held = p.tokens - out.reduce((a, x) => a + (x.kind === 'sell' ? x.tokens : 0), 0);
|
|
113
|
+
// 4. Take-profit ladder (fractions of the ORIGINAL size; several steps may fill on one gap).
|
|
114
|
+
let left = held;
|
|
115
|
+
while (p.tpFilled < prof.tpLadder.length && gain >= prof.tpLadder[p.tpFilled].atPct) {
|
|
116
|
+
const step = prof.tpLadder[p.tpFilled];
|
|
117
|
+
const tokens = Math.min(left, p.origTokens * step.sellFrac);
|
|
118
|
+
p.tpFilled++;
|
|
119
|
+
if (tokens <= 0)
|
|
120
|
+
continue;
|
|
121
|
+
left -= tokens;
|
|
122
|
+
out.push({ kind: 'sell', tokens, all: left <= p.origTokens * 1e-6, reason: `tp${p.tpFilled}`, detail: `${pct(gain)} ≥ +${step.atPct}%: sell ${Math.round(step.sellFrac * 100)}% of the original size` });
|
|
123
|
+
if (prof.wake?.onTpFilled)
|
|
124
|
+
wake('tp_filled', `TP${p.tpFilled} filled at ${pct(gain)}; ${Math.round((left / p.origTokens) * 100)}% of the original size left`, ['hold', 'exit', 'tighten', 'reprofile']);
|
|
125
|
+
}
|
|
126
|
+
if (left <= p.origTokens * 1e-6)
|
|
127
|
+
return out;
|
|
128
|
+
// 5a. Bag trail (exit profile 1.2.0): armed by the cost-recovery sell (and, with armAtPct, a higher peak),
|
|
129
|
+
// measured off the peak since entry.
|
|
130
|
+
if (prof.bagTrail && p.costRecovered && peakGain >= (prof.bagTrail.armAtPct ?? 0)) {
|
|
131
|
+
const off = ((p.peakPrice - k.price) / p.peakPrice) * 100;
|
|
132
|
+
const trail = p.bagTrailPct ?? prof.bagTrail.trailPct;
|
|
133
|
+
if (off >= trail) {
|
|
134
|
+
out.push({ kind: 'sell', tokens: left, all: true, reason: 'bag-trail', detail: `the bag is ${off.toFixed(0)}% off its peak (${pct(peakGain)}), bag trail ${trail}%` });
|
|
135
|
+
out.push({ kind: 'wake', wake: { event: 'mechanical_exit', positionId: p.id, mint: p.mint, t: k.t, detail: `bag-trail: ${off.toFixed(0)}% off the peak`, allowed: ['hold'] } });
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 5. Trailing stop.
|
|
140
|
+
const ts = prof.trailingStop;
|
|
141
|
+
if (ts) {
|
|
142
|
+
if (!p.trailArmed && peakGain >= ts.armAtPct)
|
|
143
|
+
p.trailArmed = true;
|
|
144
|
+
const off = ((p.peakPrice - k.price) / p.peakPrice) * 100;
|
|
145
|
+
if (p.trailArmed && off >= ts.trailPct) {
|
|
146
|
+
out.push({ kind: 'sell', tokens: left, all: true, reason: 'trail', detail: `${off.toFixed(0)}% off the peak (${pct(peakGain)}), trail ${ts.trailPct}%` });
|
|
147
|
+
out.push({ kind: 'wake', wake: { event: 'mechanical_exit', positionId: p.id, mint: p.mint, t: k.t, detail: `trail: ${off.toFixed(0)}% off the peak`, allowed: ['hold'] } });
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 6. Time stop.
|
|
152
|
+
const tm = prof.timeStop;
|
|
153
|
+
if (tm && k.t - p.openedAt >= tm.minutes * 60e3 && gain < tm.ifBelowPct) {
|
|
154
|
+
if (tm.action === 'exit') {
|
|
155
|
+
out.push({ kind: 'sell', tokens: left, all: true, reason: 'time', detail: `${tm.minutes} min in at ${pct(gain)} (needed +${tm.ifBelowPct}%)` });
|
|
156
|
+
out.push({ kind: 'wake', wake: { event: 'mechanical_exit', positionId: p.id, mint: p.mint, t: k.t, detail: `time: ${tm.minutes} min at ${pct(gain)}`, allowed: ['hold'] } });
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
if (!p.wakeSent.time && (prof.wake?.onTimeReview ?? true)) {
|
|
160
|
+
p.wakeSent.time = true;
|
|
161
|
+
wake(tm.action === 'review' ? 'time_review' : 'time_tighten', `${tm.minutes} min in at ${pct(gain)}, below the +${tm.ifBelowPct}% the profile wanted by now`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// 7. Judgement wakes (once each).
|
|
165
|
+
const w = prof.wake;
|
|
166
|
+
if (w?.drawdownPct != null && gain <= w.drawdownPct && !p.wakeSent.drawdown) {
|
|
167
|
+
p.wakeSent.drawdown = true;
|
|
168
|
+
wake('drawdown', `position at ${pct(gain)}; no hard stop in this profile`);
|
|
169
|
+
}
|
|
170
|
+
if (w?.thesisWarnFrac != null && liqDrop >= tb.liquidityDropPct * w.thesisWarnFrac && !p.wakeSent.thesis) {
|
|
171
|
+
p.wakeSent.thesis = true;
|
|
172
|
+
wake('thesis_warning', `liquidity down ${liqDrop.toFixed(0)}% from its window high (exit at ${tb.liquidityDropPct}%)`);
|
|
173
|
+
}
|
|
174
|
+
// 8. Ladder adds (scale-in tranches), each still clamped by the runner.
|
|
175
|
+
p.ladder.forEach((tr, i) => {
|
|
176
|
+
if (tr.usd <= 0)
|
|
177
|
+
return;
|
|
178
|
+
if (k.t > tr.deadline) {
|
|
179
|
+
tr.usd = 0;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const first = p.fills[0].price;
|
|
183
|
+
const move = ((k.price - first) / first) * 100;
|
|
184
|
+
if (tr.atPct < 0 ? move <= tr.atPct : move >= tr.atPct) {
|
|
185
|
+
out.push({ kind: 'add', index: i, usd: tr.usd, detail: `tranche ${i + 1}: ${pct(move)} from the first fill (trigger ${tr.atPct}%)` });
|
|
186
|
+
tr.usd = 0;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// dexterm autotrade — candidate and position sessions on a LOCAL model (task-1543, Ollama).
|
|
2
|
+
//
|
|
3
|
+
// The same protocol as the claude sessions (sessions.ts, AUTOTRADE-SPEC §4): one conversation per coin, the
|
|
4
|
+
// DOSSIER first, then DELTA / ENGINE / EVENT messages, and every reply ends in one `DECISION {json}` line that
|
|
5
|
+
// session.ts parses against the closed schema and the engine clamps. What differs is only who answers:
|
|
6
|
+
//
|
|
7
|
+
// * the session is TOOL-LESS by construction: no `tools` are ever sent to /api/chat (the test asserts the
|
|
8
|
+
// request body), so the model can only answer;
|
|
9
|
+
// * the reply is constrained with Ollama structured output ({ comment, decision }) and rendered back into the
|
|
10
|
+
// exact text form the claude path produces, so parseDecision, the clamp, the caps and the ledger are shared;
|
|
11
|
+
// * a reply later than decideWithinSec aborts the request and resolves as no answer (no late result exists);
|
|
12
|
+
// * cost is $0 (the user's own machine); the day turn / session caps of the SessionGate still apply.
|
|
13
|
+
//
|
|
14
|
+
// The restriction profile is checked when the factory is built: autotrade on a local engine needs `paper`
|
|
15
|
+
// (`full` for anything live). Nothing here sees a key, a wallet or the signer.
|
|
16
|
+
import { OllamaError, atLeast, requiredLevel, stripThink } from '../../mcp/src/engine/index.js';
|
|
17
|
+
import { ACTIONS, parseDecision } from "./session.js";
|
|
18
|
+
/** The structured-output schema of one reply: a short comment and the decision object (the closed §4 shape). */
|
|
19
|
+
export const OLLAMA_DECISION_FORMAT = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
comment: { type: 'string' },
|
|
23
|
+
decision: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
action: { type: 'string', enum: [...ACTIONS] },
|
|
27
|
+
profile: { type: 'string' },
|
|
28
|
+
sizePct: { type: 'number' },
|
|
29
|
+
confidence: { type: 'number' },
|
|
30
|
+
fraction: { type: 'number' },
|
|
31
|
+
reason: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
required: ['action', 'reason'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ['comment', 'decision'],
|
|
37
|
+
};
|
|
38
|
+
/** Render a structured reply as the claude-path text: comment lines, then one DECISION line. Raw text is kept as is. */
|
|
39
|
+
export function renderOllamaReply(content) {
|
|
40
|
+
const s = stripThink(content).trim();
|
|
41
|
+
let v;
|
|
42
|
+
try {
|
|
43
|
+
v = JSON.parse(s);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return s;
|
|
47
|
+
}
|
|
48
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
49
|
+
return s;
|
|
50
|
+
const o = v;
|
|
51
|
+
if (!o.decision || typeof o.decision !== 'object')
|
|
52
|
+
return s;
|
|
53
|
+
// Drop keys the model set to null (optional fields), keep everything else for the closed-schema check to judge.
|
|
54
|
+
const d = Object.fromEntries(Object.entries(o.decision).filter(([, x]) => x !== null));
|
|
55
|
+
const comment = typeof o.comment === 'string' ? o.comment.replace(/\n+/g, ' ').trim().slice(0, 400) : '';
|
|
56
|
+
return `${comment}\nDECISION ${JSON.stringify(d)}`.trim();
|
|
57
|
+
}
|
|
58
|
+
class OllamaSession {
|
|
59
|
+
id;
|
|
60
|
+
kind = 'ollama';
|
|
61
|
+
model;
|
|
62
|
+
o;
|
|
63
|
+
messages;
|
|
64
|
+
turns = 0;
|
|
65
|
+
closed = false;
|
|
66
|
+
why = null;
|
|
67
|
+
inflight = null;
|
|
68
|
+
constructor(id, o) {
|
|
69
|
+
this.id = id;
|
|
70
|
+
this.o = o;
|
|
71
|
+
this.model = o.model;
|
|
72
|
+
this.messages = [{ role: 'system', content: o.systemPrompt }];
|
|
73
|
+
}
|
|
74
|
+
costUsd() { return 0; }
|
|
75
|
+
endReason() { return this.why; }
|
|
76
|
+
turnsUsed() { return this.turns; }
|
|
77
|
+
async send(text) {
|
|
78
|
+
if (this.closed || this.why)
|
|
79
|
+
return null;
|
|
80
|
+
if (this.turns >= this.o.maxTurns)
|
|
81
|
+
return null;
|
|
82
|
+
this.turns++;
|
|
83
|
+
const t0 = Date.now();
|
|
84
|
+
this.messages.push({ role: 'user', content: text });
|
|
85
|
+
const ac = new AbortController();
|
|
86
|
+
this.inflight = ac;
|
|
87
|
+
const timer = setTimeout(() => ac.abort(), this.o.decideWithinMs);
|
|
88
|
+
timer.unref?.();
|
|
89
|
+
try {
|
|
90
|
+
// Tool-less: the request never carries `tools`.
|
|
91
|
+
const r = await this.o.client.chat({ model: this.o.model, messages: this.messages, format: OLLAMA_DECISION_FORMAT, options: { num_ctx: this.o.numCtx } }, { signal: ac.signal, timeoutMs: this.o.decideWithinMs + 1000 });
|
|
92
|
+
this.o.onRaw?.(JSON.stringify({ engine: 'ollama', model: r.model ?? this.o.model, content: r.content.slice(0, 4000), promptTokens: r.promptTokens, evalTokens: r.evalTokens }));
|
|
93
|
+
const reply = renderOllamaReply(r.content);
|
|
94
|
+
this.messages.push({ role: 'assistant', content: reply });
|
|
95
|
+
return { text: reply, decision: parseDecision(reply), costUsd: 0, ms: Date.now() - t0 };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
// No answer in time (or the server went away): the turn resolves as no answer, never as a late decision.
|
|
99
|
+
this.messages.push({ role: 'assistant', content: '(no answer in time)' });
|
|
100
|
+
if (e instanceof OllamaError && (e.kind === 'unreachable' || e.kind === 'model-missing'))
|
|
101
|
+
this.why = e.kind === 'unreachable' ? 'ollama-unreachable' : 'model-missing';
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
if (this.inflight === ac)
|
|
107
|
+
this.inflight = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async close(reason) {
|
|
111
|
+
this.closed = true;
|
|
112
|
+
try {
|
|
113
|
+
this.inflight?.abort();
|
|
114
|
+
}
|
|
115
|
+
catch { /* ignore */ }
|
|
116
|
+
this.why ??= reason;
|
|
117
|
+
return { turns: this.turns, costUsd: 0, reason };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Throws when the profile does not allow autotrade on a local engine (the run refuses to start). */
|
|
121
|
+
export function assertAutotradeAllowed(restriction, live = false) {
|
|
122
|
+
const need = requiredLevel('autotrade', { live });
|
|
123
|
+
if (!atLeast(restriction, need))
|
|
124
|
+
throw new Error(`the local engine's restriction profile is ${restriction}; ${live ? 'a live' : 'an autotrade'} run needs ${need} (dexterm ai restrictions ${need}${need === 'full' ? ', or dexterm ai engine ollama --full' : ''})`);
|
|
125
|
+
}
|
|
126
|
+
/** The runner's session factory on the user's local Ollama: same signature and protocol as claudeSessionFactory. */
|
|
127
|
+
export function ollamaSessionFactory(f) {
|
|
128
|
+
assertAutotradeAllowed(f.restriction, f.live);
|
|
129
|
+
return (id, s, ctx) => {
|
|
130
|
+
f.log('session_init', { sessionId: id, sessionKind: ctx.kind, engine: 'ollama', model: f.model, host: f.client.host.local ? 'local' : 'remote', tools: 0, mcpServers: 0, restriction: f.restriction, costUsd: 0, ok: true });
|
|
131
|
+
return new OllamaSession(id, {
|
|
132
|
+
client: f.client, model: f.model, numCtx: f.numCtx, systemPrompt: f.systemPrompt(s),
|
|
133
|
+
maxTurns: s.agent.maxTurns, decideWithinMs: s.agent.decideWithinSec * 1000,
|
|
134
|
+
onRaw: f.onRaw ? (line) => f.onRaw(id, line) : undefined,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// dexterm autotrade — paper ACCOUNTS: several strategies trading from one simulated wallet (task-1538). PAPER only.
|
|
2
|
+
//
|
|
3
|
+
// Without an account every strategy of a parallel run owns its bankroll (its own SimBook, its own cash). A paper
|
|
4
|
+
// account is one shared pool of simulated cash that any number of strategies trade from, the way several bots would
|
|
5
|
+
// share one real wallet:
|
|
6
|
+
//
|
|
7
|
+
// account "main" ($1000) ── strategy moon (own positions, loss streak, per-coin rules, entries/day)
|
|
8
|
+
// └─ strategy scalp (own positions, …)
|
|
9
|
+
// account "yolo" (1 SOL) ── strategy scalp again (the same rules, another account: its own member, ledger and book)
|
|
10
|
+
//
|
|
11
|
+
// What is shared: the cash (a fill spends from, and a sale pays into, the account), the equity every strategy sizes
|
|
12
|
+
// from (cash + every open cost basis on the account, so "5% of the bankroll" means 5% of the wallet), and the
|
|
13
|
+
// drawdown / day-start figures each strategy's breaker reads (the wallet's drawdown halts each strategy at its own
|
|
14
|
+
// threshold). What stays per strategy: positions, exposure (its own footprint against the wallet), loss streak,
|
|
15
|
+
// cooldowns, per-coin limits, entry counts, its session caps and its P/L.
|
|
16
|
+
//
|
|
17
|
+
// Nothing here holds a key or touches a wallet: an account is a number in memory for the length of one run.
|
|
18
|
+
/** How many paper accounts one parallel run holds at most. */
|
|
19
|
+
export const MAX_PAPER_ACCOUNTS = 8;
|
|
20
|
+
const r2 = (v) => Math.round(v * 100) / 100;
|
|
21
|
+
export class PaperAccount {
|
|
22
|
+
id;
|
|
23
|
+
startUsd;
|
|
24
|
+
sol;
|
|
25
|
+
solUsd;
|
|
26
|
+
cashUsd;
|
|
27
|
+
books = [];
|
|
28
|
+
constructor(id, startUsd, o = {}) {
|
|
29
|
+
if (!(startUsd > 0) || !Number.isFinite(startUsd))
|
|
30
|
+
throw new Error(`paper account ${id}: the starting balance must be a positive number`);
|
|
31
|
+
this.id = id;
|
|
32
|
+
this.startUsd = startUsd;
|
|
33
|
+
this.cashUsd = startUsd;
|
|
34
|
+
if (o.sol !== undefined) {
|
|
35
|
+
this.sol = o.sol;
|
|
36
|
+
this.solUsd = o.solUsd;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Cash held for fills in flight (an executor is async: the debit lands after it answers). */
|
|
40
|
+
held = 0;
|
|
41
|
+
/** Hold `usd` of the cash for one fill; false when the cash (less what other fills hold) is not there. */
|
|
42
|
+
hold(usd) {
|
|
43
|
+
if (!(usd >= 0) || usd > this.cashUsd - this.held + 1e-9)
|
|
44
|
+
return false;
|
|
45
|
+
this.held += usd;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/** Release a hold (the fill was booked, synchronously after the executor answered, or refused). */
|
|
49
|
+
release(usd) { this.held = Math.max(0, this.held - usd); }
|
|
50
|
+
/** A strategy's book joins the account (SimBook's constructor calls this). A book never leaves: a stopped strategy's bags stay on the account. */
|
|
51
|
+
join(book, member = book.strategy.id) { this.books.push({ member, book }); }
|
|
52
|
+
/** Cash + every open cost basis (unrealized gains do not count for sizing). */
|
|
53
|
+
bookEquity() { let e = this.cashUsd; for (const b of this.books)
|
|
54
|
+
e += b.book.openCostUsd(); return e; }
|
|
55
|
+
/** Cash + every open bag marked at what it would sell for now. */
|
|
56
|
+
markedEquity() { let e = this.cashUsd; for (const b of this.books)
|
|
57
|
+
e += b.book.openMarkUsd(); return e; }
|
|
58
|
+
view() {
|
|
59
|
+
const eq = this.markedEquity();
|
|
60
|
+
const pnl = r2(eq - this.startUsd);
|
|
61
|
+
let open = 0, positions = 0;
|
|
62
|
+
for (const b of this.books) {
|
|
63
|
+
open += b.book.openCostUsd();
|
|
64
|
+
positions += b.book.open.size;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: this.id, startUsd: r2(this.startUsd), cashUsd: r2(this.cashUsd), equityUsd: r2(eq), pnlUsd: pnl, pnlPct: Math.round((pnl / this.startUsd) * 1000) / 10,
|
|
68
|
+
openCostUsd: r2(open), positions, strategies: [...new Set(this.books.map((b) => b.member))],
|
|
69
|
+
...(this.sol !== undefined ? { sol: this.sol, solUsd: this.solUsd } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// dexterm autotrade — the replay tape (task-1498 spike, AUTOTRADE-SPEC §3).
|
|
2
|
+
//
|
|
3
|
+
// A tape is recorded market history in time order: one row per (mint, t) taken from the recorder's
|
|
4
|
+
// `token_snapshots` (the app's own SQLite file, read-only). The same MarketTick type feeds the live screener
|
|
5
|
+
// daemon, the paper runner, the monitor and the backtester, so a backtest replays exactly what a live run
|
|
6
|
+
// would have seen. `tapeFromDb` extracts one; `saveTape` / `loadTape` keep a compact committed fixture.
|
|
7
|
+
//
|
|
8
|
+
// Honest limits of the recorded data (written into every report): rows are written on material change
|
|
9
|
+
// (≥ 5 % price/liquidity) or every 30 min, at the app's ~2-minute refresh cadence, and only while a coin is on
|
|
10
|
+
// a screen, plus sweep re-observations at 1 h…72 h. A gap therefore means "moved less than ~5 %" while the coin
|
|
11
|
+
// was listed, and "unknown" after it left. There are no trades or holder lists in the history.
|
|
12
|
+
//
|
|
13
|
+
// task-1504: where the recorder's tick lane ran (`autotrade_ticks`: a sample every 5–10 s for watchlist / open /
|
|
14
|
+
// candidate / held coins), `tapeFromDb` merges those samples in tape order (t, a snapshot row before a tick at the
|
|
15
|
+
// same t). A tick carries price, liquidity, mcap and the 5 m / 1 h windows; the slower fields (24 h windows, holders,
|
|
16
|
+
// top-10, dev, authorities, organic score) are carried forward from the mint's latest earlier snapshot, never from a
|
|
17
|
+
// later one, and the resolved mcap is scaled by the price move. Coins without ticks replay exactly as before.
|
|
18
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { deriveFlags } from "../../cli/src/screener/rows.js";
|
|
20
|
+
export const TAPE_SCHEMA = 'dexterm-autotrade-tape/1';
|
|
21
|
+
/** Column order of a committed tape row (compact arrays keep fixtures small). */
|
|
22
|
+
export const TAPE_COLUMNS = [
|
|
23
|
+
't', 'kind', 'price', 'mcap', 'mcapResolved', 'mcapFlagged', 'liq', 'vol5m', 'vol1h', 'vol24', 'buys5m', 'sells5m', 'buys24', 'sells24',
|
|
24
|
+
'chg5m', 'chg1h', 'chg6h', 'chg24h', 'holders', 'top10', 'dev', 'mintAuth', 'freezeAuth', 'org',
|
|
25
|
+
];
|
|
26
|
+
const SIG = 6;
|
|
27
|
+
const sig = (x) => (x === null || !Number.isFinite(x) ? null : Number(x.toPrecision(SIG)));
|
|
28
|
+
const bool = (x) => (x === null || x === undefined ? null : Boolean(x));
|
|
29
|
+
const numOrNull = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
30
|
+
const SNAP_COLS = `t, kind, symbol, launchpad, created_at, pair_created_at, price_usd, mcap, mcap_resolved, mcap_flagged, liquidity_usd,
|
|
31
|
+
vol_m5, vol_h1, vol_h24, buys_m5, sells_m5, buys_h24, sells_h24, chg_m5, chg_h1, chg_h6, chg_h24,
|
|
32
|
+
holder_count, top_holders_pct, dev_pct, mint_auth_enabled, freeze_auth_enabled, organic_score`;
|
|
33
|
+
function hasTicks(db, since, until) {
|
|
34
|
+
const t = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'autotrade_ticks'").get();
|
|
35
|
+
return !!t && !!db.prepare('SELECT 1 FROM autotrade_ticks WHERE t >= ? AND t <= ? LIMIT 1').get(since, until);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Merge a mint's snapshot rows (time order) with its tick rows (time order) into snapshot-shaped rows: a tick takes
|
|
39
|
+
* its own price / liquidity / mcap / 5 m + 1 h windows and carries the rest forward from the latest snapshot at or
|
|
40
|
+
* before it (null before the first one).
|
|
41
|
+
*/
|
|
42
|
+
function mergeTicks(snaps, ticks) {
|
|
43
|
+
const out = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
let base = null;
|
|
46
|
+
for (const k of ticks) {
|
|
47
|
+
while (i < snaps.length && snaps[i].t <= k.t) {
|
|
48
|
+
base = snaps[i];
|
|
49
|
+
out.push(snaps[i++]);
|
|
50
|
+
}
|
|
51
|
+
const price = numOrNull(k.price_usd);
|
|
52
|
+
const basePrice = base ? numOrNull(base.price_usd) : null;
|
|
53
|
+
const baseResolved = base ? numOrNull(base.mcap_resolved) : null;
|
|
54
|
+
out.push({
|
|
55
|
+
t: k.t, kind: 'live',
|
|
56
|
+
symbol: base?.symbol ?? null, launchpad: base?.launchpad ?? null, created_at: base?.created_at ?? null, pair_created_at: k.pair_created_at ?? base?.pair_created_at ?? null,
|
|
57
|
+
price_usd: price, mcap: numOrNull(k.mcap),
|
|
58
|
+
mcap_resolved: baseResolved !== null && basePrice && price !== null ? baseResolved * (price / basePrice) : null,
|
|
59
|
+
mcap_flagged: base?.mcap_flagged ?? 0, liquidity_usd: numOrNull(k.liquidity_usd),
|
|
60
|
+
vol_m5: k.vol_m5, vol_h1: k.vol_h1, vol_h24: base?.vol_h24 ?? null, buys_m5: k.buys_m5, sells_m5: k.sells_m5, buys_h24: base?.buys_h24 ?? null, sells_h24: base?.sells_h24 ?? null,
|
|
61
|
+
chg_m5: k.chg_m5, chg_h1: k.chg_h1, chg_h6: base?.chg_h6 ?? null, chg_h24: base?.chg_h24 ?? null,
|
|
62
|
+
holder_count: base?.holder_count ?? null, top_holders_pct: base?.top_holders_pct ?? null, dev_pct: base?.dev_pct ?? null,
|
|
63
|
+
mint_auth_enabled: base?.mint_auth_enabled ?? null, freeze_auth_enabled: base?.freeze_auth_enabled ?? null, organic_score: base?.organic_score ?? null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
while (i < snaps.length)
|
|
67
|
+
out.push(snaps[i++]);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract a tape from the recorder tables (read-only handle from `openHistoryReadOnly`): `token_snapshots` and, where
|
|
72
|
+
* the tick lane ran, `autotrade_ticks` (task-1504), in tape order. A mint qualifies on its live rows of either kind.
|
|
73
|
+
*/
|
|
74
|
+
export function tapeFromDb(db, opts = {}) {
|
|
75
|
+
const since = opts.since ?? 0;
|
|
76
|
+
const until = opts.until ?? Number.MAX_SAFE_INTEGER;
|
|
77
|
+
const minLive = opts.minLiveRows ?? 20;
|
|
78
|
+
const minPeak = opts.minPeakLiqUsd ?? 20_000;
|
|
79
|
+
const span = (opts.spanHours ?? 24) * 3600e3;
|
|
80
|
+
const withTicks = opts.ticks !== false && hasTicks(db, since, until);
|
|
81
|
+
// Without ticks in the window the pick is the exact pre-1504 query, so earlier backtests reproduce bit for bit.
|
|
82
|
+
const picked = opts.mints ?? db.prepare(withTicks
|
|
83
|
+
? `SELECT mint, SUM(n) AS n, MAX(peak) AS peak, MIN(first) AS first FROM (
|
|
84
|
+
SELECT mint, COUNT(*) AS n, MAX(liquidity_usd) AS peak, MIN(t) AS first FROM token_snapshots
|
|
85
|
+
WHERE kind = 'live' AND t >= ? AND t <= ? AND price_usd > 0 GROUP BY mint
|
|
86
|
+
UNION ALL
|
|
87
|
+
SELECT mint, COUNT(*) AS n, MAX(liquidity_usd) AS peak, MIN(t) AS first FROM autotrade_ticks
|
|
88
|
+
WHERE t >= ? AND t <= ? AND price_usd > 0 GROUP BY mint
|
|
89
|
+
) GROUP BY mint HAVING n >= ? AND peak >= ? ORDER BY first, mint LIMIT ?`
|
|
90
|
+
: `SELECT mint, COUNT(*) AS n, MAX(liquidity_usd) AS peak, MIN(t) AS first FROM token_snapshots
|
|
91
|
+
WHERE kind = 'live' AND t >= ? AND t <= ? AND price_usd > 0 GROUP BY mint
|
|
92
|
+
HAVING n >= ? AND peak >= ? ORDER BY first LIMIT ?`).all(...(withTicks ? [since, until, since, until] : [since, until]), minLive, minPeak, opts.maxMints ?? 100_000).map((r) => r.mint);
|
|
93
|
+
const rowsQ = db.prepare(`SELECT ${SNAP_COLS} FROM token_snapshots WHERE mint = ? AND t >= ? AND t <= ? ORDER BY t, snap_id`);
|
|
94
|
+
const ticksQ = withTicks ? db.prepare(`SELECT t, price_usd, liquidity_usd, mcap, vol_m5, vol_h1, buys_m5, sells_m5, chg_m5, chg_h1, pair_created_at
|
|
95
|
+
FROM autotrade_ticks WHERE mint = ? AND t >= ? AND t <= ? ORDER BY t`) : null;
|
|
96
|
+
// The carry-forward base may sit before `since`: the mint's latest snapshot at or before the window start.
|
|
97
|
+
const baseQ = withTicks ? db.prepare(`SELECT ${SNAP_COLS} FROM token_snapshots WHERE mint = ? AND t < ? ORDER BY t DESC, snap_id DESC LIMIT 1`) : null;
|
|
98
|
+
const mints = [];
|
|
99
|
+
let from = Number.MAX_SAFE_INTEGER, to = 0;
|
|
100
|
+
for (const mint of picked) {
|
|
101
|
+
let rs = rowsQ.all(mint, since, until);
|
|
102
|
+
if (ticksQ) {
|
|
103
|
+
const ks = ticksQ.all(mint, since, until);
|
|
104
|
+
if (ks.length) {
|
|
105
|
+
const prior = baseQ.get(mint, since);
|
|
106
|
+
rs = mergeTicks(prior ? [prior, ...rs] : rs, ks);
|
|
107
|
+
if (prior)
|
|
108
|
+
rs = rs.filter((r) => r !== prior);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const first = rs.find((r) => r.kind === 'live');
|
|
112
|
+
if (!first)
|
|
113
|
+
continue;
|
|
114
|
+
const t0 = first.t;
|
|
115
|
+
const kept = rs.filter((r) => r.t >= t0 && r.t <= t0 + span && numOrNull(r.price_usd) !== null);
|
|
116
|
+
if (!kept.length)
|
|
117
|
+
continue;
|
|
118
|
+
// Identity from the first row that has one (a mint seen by ticks before any snapshot has none on its first row).
|
|
119
|
+
const named = rs.find((r) => r.symbol !== null && r.symbol !== undefined) ?? first;
|
|
120
|
+
const created = numOrNull(named.created_at) ?? numOrNull(first.created_at) ?? numOrNull(first.pair_created_at);
|
|
121
|
+
mints.push({
|
|
122
|
+
mint,
|
|
123
|
+
symbol: String(named.symbol ?? '?').slice(0, 16),
|
|
124
|
+
launchpad: String(named.launchpad ?? 'unknown'),
|
|
125
|
+
createdAt: created,
|
|
126
|
+
rows: kept.map((r) => [
|
|
127
|
+
r.t, r.kind, sig(numOrNull(r.price_usd)), sig(numOrNull(r.mcap)), sig(numOrNull(r.mcap_resolved)), r.mcap_flagged ? 1 : 0, sig(numOrNull(r.liquidity_usd)),
|
|
128
|
+
sig(numOrNull(r.vol_m5)), sig(numOrNull(r.vol_h1)), sig(numOrNull(r.vol_h24)), numOrNull(r.buys_m5), numOrNull(r.sells_m5), numOrNull(r.buys_h24), numOrNull(r.sells_h24),
|
|
129
|
+
sig(numOrNull(r.chg_m5)), sig(numOrNull(r.chg_h1)), sig(numOrNull(r.chg_h6)), sig(numOrNull(r.chg_h24)),
|
|
130
|
+
numOrNull(r.holder_count), sig(numOrNull(r.top_holders_pct)), sig(numOrNull(r.dev_pct)),
|
|
131
|
+
r.mint_auth_enabled === null ? null : r.mint_auth_enabled ? 1 : 0, r.freeze_auth_enabled === null ? null : r.freeze_auth_enabled ? 1 : 0, sig(numOrNull(r.organic_score)),
|
|
132
|
+
]),
|
|
133
|
+
});
|
|
134
|
+
from = Math.min(from, kept[0].t);
|
|
135
|
+
to = Math.max(to, kept[kept.length - 1].t);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
schema: TAPE_SCHEMA,
|
|
139
|
+
source: withTicks ? 'recorder token_snapshots + autotrade_ticks (read-only)' : 'recorder token_snapshots (read-only)',
|
|
140
|
+
extractedAt: new Date().toISOString(),
|
|
141
|
+
window: { from: mints.length ? from : 0, to },
|
|
142
|
+
note: withTicks
|
|
143
|
+
? 'Change-driven rows while listed, sweep re-observations, and 5-10 s tick samples for focus coins (slow fields carried forward). No trades.'
|
|
144
|
+
: 'Change-driven rows at the app refresh cadence while listed, plus sweep re-observations. No candles or trades.',
|
|
145
|
+
columns: TAPE_COLUMNS,
|
|
146
|
+
mints,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function saveTape(path, doc) {
|
|
150
|
+
// One mint row array per line: diffable and still small.
|
|
151
|
+
const head = { ...doc, mints: undefined };
|
|
152
|
+
const lines = doc.mints.map((m) => ` ${JSON.stringify(m)}`).join(',\n');
|
|
153
|
+
const json = JSON.stringify(head, null, 2).replace(/\n}$/, `,\n "mints": [\n${lines}\n ]\n}\n`);
|
|
154
|
+
writeFileSync(path, json);
|
|
155
|
+
}
|
|
156
|
+
export function readTapeDoc(path) {
|
|
157
|
+
const doc = JSON.parse(readFileSync(path, 'utf8'));
|
|
158
|
+
if (doc.schema !== TAPE_SCHEMA)
|
|
159
|
+
throw new Error(`not a ${TAPE_SCHEMA} file`);
|
|
160
|
+
return doc;
|
|
161
|
+
}
|
|
162
|
+
/** Expand a tape document into ticks in deterministic time order (t, then mint). */
|
|
163
|
+
export function tapeFromDoc(doc) {
|
|
164
|
+
const col = new Map(doc.columns.map((c, i) => [c, i]));
|
|
165
|
+
const at = (r, k) => r[col.get(k) ?? -1];
|
|
166
|
+
const ticks = [];
|
|
167
|
+
for (const m of doc.mints) {
|
|
168
|
+
for (const r of m.rows) {
|
|
169
|
+
ticks.push({
|
|
170
|
+
mint: m.mint, t: at(r, 't'), kind: at(r, 'kind') === 'sweep' ? 'sweep' : 'live',
|
|
171
|
+
symbol: m.symbol, launchpad: m.launchpad, createdAt: m.createdAt,
|
|
172
|
+
price: numOrNull(at(r, 'price')), mcap: numOrNull(at(r, 'mcap')), mcapResolved: numOrNull(at(r, 'mcapResolved')), mcapFlagged: Boolean(at(r, 'mcapFlagged')),
|
|
173
|
+
liq: numOrNull(at(r, 'liq')), vol5m: numOrNull(at(r, 'vol5m')), vol1h: numOrNull(at(r, 'vol1h')), vol24: numOrNull(at(r, 'vol24')),
|
|
174
|
+
buys5m: numOrNull(at(r, 'buys5m')), sells5m: numOrNull(at(r, 'sells5m')), buys24: numOrNull(at(r, 'buys24')), sells24: numOrNull(at(r, 'sells24')),
|
|
175
|
+
chg5m: numOrNull(at(r, 'chg5m')), chg1h: numOrNull(at(r, 'chg1h')), chg6h: numOrNull(at(r, 'chg6h')), chg24h: numOrNull(at(r, 'chg24h')),
|
|
176
|
+
holders: numOrNull(at(r, 'holders')), top10: numOrNull(at(r, 'top10')), dev: numOrNull(at(r, 'dev')),
|
|
177
|
+
mintAuth: bool(at(r, 'mintAuth')), freezeAuth: bool(at(r, 'freezeAuth')), org: numOrNull(at(r, 'org')),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
ticks.sort((a, b) => a.t - b.t || (a.mint < b.mint ? -1 : a.mint > b.mint ? 1 : 0));
|
|
182
|
+
return { source: doc.source, ticks, mints: doc.mints.map((m) => m.mint) };
|
|
183
|
+
}
|
|
184
|
+
export const loadTape = (path) => tapeFromDoc(readTapeDoc(path));
|
|
185
|
+
/** The screener's row shape for a tick, so strategies reuse the exact filter grammar of the screen. */
|
|
186
|
+
export function tickRow(k) {
|
|
187
|
+
const base = { mintAuth: k.mintAuth, freezeAuth: k.freezeAuth, top10: k.top10, dev: k.dev, bundled: null, lpLocked: k.launchpad === 'pump.fun' ? 100 : null };
|
|
188
|
+
return {
|
|
189
|
+
mint: k.mint, symbol: k.symbol, name: '', launchpad: k.launchpad,
|
|
190
|
+
price: k.price, mcap: k.mcap, mcapFlagged: k.mcapFlagged, fdv: null, liq: k.liq,
|
|
191
|
+
vol24: k.vol24, vol6h: null, vol1h: k.vol1h, vol5m: k.vol5m,
|
|
192
|
+
ageMin: k.createdAt ? Math.max(0, (k.t - k.createdAt) / 60000) : null,
|
|
193
|
+
holders: k.holders, holders24: null, holdersChgPct: null,
|
|
194
|
+
chg: { m5: k.chg5m, h1: k.chg1h, h6: k.chg6h, h24: k.chg24h }, spark: undefined,
|
|
195
|
+
buys: k.buys24, sells: k.sells24, org: k.org, ...base, snipers: null, flags: deriveFlags(base),
|
|
196
|
+
verified: false, sus: false, bonding: null, pair: null, sources: [], stale: k.kind === 'sweep', fetchedAt: k.t, aiNote: null, image: null,
|
|
197
|
+
};
|
|
198
|
+
}
|