dexterm-cli 0.1.5 → 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/index.js +2 -0
- package/dist/packages/agents/src/schema.js +22 -1
- package/dist/packages/agents/src/spectator.js +54 -0
- package/dist/packages/agents/src/story.js +532 -0
- package/dist/packages/agents/src/tiers.js +28 -2
- package/dist/packages/agents/src/tools.js +31 -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/REGISTRY.json +1 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
- package/dist/packages/cli/src/agent-cmd.js +6 -4
- package/dist/packages/cli/src/agent-spectator.js +17 -1
- package/dist/packages/cli/src/ai-cmd.js +412 -0
- package/dist/packages/cli/src/analyst/backend.js +105 -15
- package/dist/packages/cli/src/analyst/codex.js +14 -9
- package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
- package/dist/packages/cli/src/analyst/ollama.js +263 -0
- package/dist/packages/cli/src/analyst/session.js +17 -3
- package/dist/packages/cli/src/app-help-gen.js +51 -15
- 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/backtest-cmd.js +13 -2
- package/dist/packages/cli/src/display.js +208 -23
- 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/dev-history.js +31 -15
- package/dist/packages/cli/src/live/index.js +28 -90
- package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
- package/dist/packages/cli/src/live/screens/token-live.js +48 -15
- package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
- package/dist/packages/cli/src/live/trade.js +30 -453
- package/dist/packages/cli/src/live/watchlist.js +1 -1
- package/dist/packages/cli/src/main.js +290 -199
- package/dist/packages/cli/src/mcp-cmd.js +7 -1
- package/dist/packages/cli/src/mcp-follow.js +103 -6
- package/dist/packages/cli/src/plan-cmd.js +15 -5
- 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 +38 -6
- package/dist/packages/cli/src/record-cmd.js +141 -20
- package/dist/packages/cli/src/scan/ui.js +7 -1
- package/dist/packages/cli/src/screens/agent-hud.js +773 -0
- package/dist/packages/cli/src/screens/analyst-session.js +11 -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 +634 -0
- package/dist/packages/cli/src/screens/newpairs.js +1 -1
- 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 +144 -0
- package/dist/packages/cli/src/screens/positions.js +6 -28
- package/dist/packages/cli/src/screens/screener.js +1 -0
- package/dist/packages/cli/src/screens/ticket.js +38 -205
- package/dist/packages/cli/src/screens/token.js +18 -9
- package/dist/packages/cli/src/screens/trending.js +1 -1
- package/dist/packages/cli/src/state.js +88 -18
- 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 +41 -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 +2 -14
- package/dist/packages/data/src/larp/score.js +220 -0
- package/dist/packages/data/src/larp/x-account.js +316 -0
- package/dist/packages/data/src/store.js +14 -5
- package/dist/packages/data/src/trade/agent/paper-desk.js +300 -23
- 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/engine.js +7 -0
- package/dist/packages/data/src/trade/plans/index.js +0 -1
- package/dist/packages/data/src/trade/plans/mandate.js +28 -15
- package/dist/packages/data/src/trade/plans/service.js +4 -28
- 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/dev-history.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
- 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/scan-mode.md +1 -1
- 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 +6 -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/spectate-an-agent.md +2 -1
- 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 +7 -3
- 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-starting-a-run.md +2 -0
- 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 +32 -0
- 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 +9 -7
- 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-running-the-tests.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp.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 +4 -13
- 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 +40 -0
- 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-market-cap-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +10 -7
- 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-follow-over-mcp-in-the-running-terminal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator.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 +5 -2
- package/dist/packages/mcp/src/server.js +285 -36
- package/dist/packages/mcp/src/spectator-sink.js +85 -0
- package/dist/packages/packs/src/backtest.js +3 -1
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/larp-score-backtest.js +119 -0
- package/dist/packages/popup/src/pages/chart-lib.js +169 -35
- package/dist/packages/popup/src/pages/chart.css +18 -0
- package/dist/packages/popup/src/pages/chart.html +6 -1
- package/dist/packages/popup/src/pages/chart.js +136 -10
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +1 -0
- package/dist/packages/popup/src/pages/spectator.js +69 -0
- package/dist/packages/popup/src/protocol.js +4 -0
- package/dist/packages/popup/src/server.js +9 -1
- package/dist/packages/recorder/src/dev-history.js +7 -3
- 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/canvas.js +7 -0
- package/dist/packages/tui/src/fmt.js +53 -0
- package/dist/packages/tui/src/index.js +1 -0
- package/dist/packages/tui/src/keymap.js +25 -1
- package/dist/packages/tui/src/markers.js +219 -0
- package/dist/packages/tui/src/widgets.js +158 -6
- 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
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
// day, skipped while the bucket is paused after a 429. The only requests this package makes.
|
|
8
8
|
// label code-only outcome labels per (t0 anchor, horizon) from the recorded numbers (labels.ts).
|
|
9
9
|
// prune retention: raw JSON 7 d (focus rows only), one row per mint per minute after 24 h,
|
|
10
|
-
// columns 90 d, outcomes/verdicts forever.
|
|
10
|
+
// columns 90 d, outcomes/verdicts forever; autotrade ticks 14 d, sampled candles 90 d.
|
|
11
|
+
// ticks (task-1504, opt-in) position-grade samples every 5–10 s for held / open / candidate / watch coins
|
|
12
|
+
// (ticks.ts), and a DB cap enforced by evicting the oldest history (retention.ts) instead of
|
|
13
|
+
// pausing observation.
|
|
11
14
|
// Everything is synchronous SQLite on the app's own DB file (node:sqlite, WAL), batched in transactions,
|
|
12
15
|
// so a refresh never awaits the recorder and a crash loses at most one batch.
|
|
13
16
|
import { mkdirSync } from 'node:fs';
|
|
@@ -18,8 +21,10 @@ import { ALL_HORIZONS_MASK, HORIZONS_SEC, SCHEMA_SQL, SCHEMA_VERSION } from "./s
|
|
|
18
21
|
import { COLUMN_NAMES, fromDsView, fromEnrichedRow, isMaterialChange } from "./snapshot.js";
|
|
19
22
|
import { LABELER_VERSION, labelAtHorizon } from "./labels.js";
|
|
20
23
|
import { dueBounds, selectDue } from "./sweep.js";
|
|
24
|
+
import { TickLane } from "./ticks.js";
|
|
25
|
+
import { DEFAULT_AGE, enforceCap, pruneTicksByAge } from "./retention.js";
|
|
21
26
|
export const DEFAULT_SWEEP = { everyMs: 60_000, maxCallsPerSweep: 6, maxCallsPerDay: 1500, batch: 30 };
|
|
22
|
-
export const DEFAULT_RETENTION = { rawDays: 7, columnsDays: 90, downsampleAfterHours: 24, everyMs: 3_600_000 };
|
|
27
|
+
export const DEFAULT_RETENTION = { rawDays: 7, columnsDays: 90, downsampleAfterHours: 24, everyMs: 3_600_000, ...DEFAULT_AGE };
|
|
23
28
|
/** Unchanged rows are re-written this often at most; the horizons are ≥ 1 h, so 30 min keeps every window observed. */
|
|
24
29
|
export const DEFAULT_HEARTBEAT_MS = 1_800_000;
|
|
25
30
|
export const DEFAULT_T0_EVERY_MS = 3_600_000;
|
|
@@ -43,6 +48,9 @@ export class Recorder {
|
|
|
43
48
|
membersEveryMs;
|
|
44
49
|
sweepPolicy;
|
|
45
50
|
retention;
|
|
51
|
+
/** task-1504: the tick lane (null when not configured) and the cap policy (null = no cap). */
|
|
52
|
+
tickLane;
|
|
53
|
+
cap;
|
|
46
54
|
onLog;
|
|
47
55
|
/**
|
|
48
56
|
* Per mint: the last WRITTEN row (columns + hash + time) and the newest observation time this process
|
|
@@ -92,6 +100,8 @@ export class Recorder {
|
|
|
92
100
|
lastLive: this.db.prepare(`SELECT t, hash, sources, ${cols} FROM token_snapshots WHERE mint = ? AND kind = 'live' ORDER BY t DESC LIMIT 1`),
|
|
93
101
|
insertCreator: this.db.prepare('INSERT OR IGNORE INTO creators (mint, creator, source, first_t, created_at) VALUES (?, ?, ?, ?, ?)'),
|
|
94
102
|
};
|
|
103
|
+
this.tickLane = opts.ticks ? new TickLane(this.db, { focus: opts.ticks.focus, policy: opts.ticks.policy, clock: this.clock, onLog: this.onLog }) : null;
|
|
104
|
+
this.cap = opts.cap && opts.cap.maxBytes ? opts.cap : null;
|
|
95
105
|
}
|
|
96
106
|
/** Attach as the data layer's store listener (and detach any previous one). */
|
|
97
107
|
attach(store) {
|
|
@@ -298,8 +308,38 @@ export class Recorder {
|
|
|
298
308
|
catch (err) {
|
|
299
309
|
this.log(`prune failed: ${msg(err)}`);
|
|
300
310
|
}
|
|
311
|
+
try {
|
|
312
|
+
this.enforceCap(now);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
this.log(`cap failed: ${msg(err)}`);
|
|
316
|
+
}
|
|
301
317
|
return out;
|
|
302
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* task-1504: one tick-lane poll (ticks.ts). The host calls it every `fastEveryMs` (5 s); 'disabled' when the
|
|
321
|
+
* recorder is off or no tick lane was configured. The only other requests this package makes besides sweeps.
|
|
322
|
+
*/
|
|
323
|
+
async pollTicks(deps, now = this.clock.now()) {
|
|
324
|
+
if (!this.enabled || this.closed || !this.tickLane)
|
|
325
|
+
return { coins: 0, calls: 0, answered: 0, written: 0, overCap: 0, skipped: 'disabled' };
|
|
326
|
+
return this.tickLane.poll(deps, now);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* task-1504: keep the DB under the configured cap by evicting the oldest history (retention.ts). A no-op without
|
|
330
|
+
* a cap or below its high-water mark. Observation never pauses; the collector calls this before each pass.
|
|
331
|
+
*/
|
|
332
|
+
enforceCap(now = this.clock.now()) {
|
|
333
|
+
if (!this.cap || this.closed)
|
|
334
|
+
return null;
|
|
335
|
+
const r = enforceCap(this.db, this.cap, now);
|
|
336
|
+
if (r.evicted) {
|
|
337
|
+
this.log(`cap: used ${(r.usedBefore / 1e6).toFixed(1)} → ${(r.usedAfter / 1e6).toFixed(1)} MB (${r.reclaimed} reclaimed, ${r.slices} slice(s) of history), history now from ${r.cutoff === null ? '—' : new Date(r.cutoff).toISOString()}${r.over ? ' (still over: only the newest hour is left)' : ''}`);
|
|
338
|
+
// Forget cached last rows: an evicted mint is re-seeded from the DB (or written fresh) next time.
|
|
339
|
+
this.seen.clear();
|
|
340
|
+
}
|
|
341
|
+
return r;
|
|
342
|
+
}
|
|
303
343
|
/** Mints whose horizon window is open and unobserved (sweep.ts), soonest-closing first. */
|
|
304
344
|
due(now = this.clock.now(), max = this.sweepPolicy.maxCallsPerSweep * this.sweepPolicy.batch) {
|
|
305
345
|
const b = dueBounds(now);
|
|
@@ -455,7 +495,7 @@ export class Recorder {
|
|
|
455
495
|
* watermark in recorder_meta means each pass only looks at the hours that newly crossed the line.
|
|
456
496
|
*/
|
|
457
497
|
prune(now = this.clock.now()) {
|
|
458
|
-
const res = { rawCleared: 0, downsampled: 0, expired: 0, membersExpired: 0 };
|
|
498
|
+
const res = { rawCleared: 0, downsampled: 0, expired: 0, membersExpired: 0, ticksExpired: 0, candlesExpired: 0 };
|
|
459
499
|
if (this.closed)
|
|
460
500
|
return res;
|
|
461
501
|
const day = 86_400_000;
|
|
@@ -480,14 +520,17 @@ export class Recorder {
|
|
|
480
520
|
res.membersExpired = Number(this.db.prepare('DELETE FROM feed_members WHERE t < ?').run(colsBefore).changes);
|
|
481
521
|
this.db.prepare('DELETE FROM mints WHERE last_any_t < ?').run(colsBefore);
|
|
482
522
|
this.db.prepare('DELETE FROM sweeps WHERE started_at < ?').run(colsBefore);
|
|
523
|
+
const tk = pruneTicksByAge(this.db, { ticksDays: this.retention.ticksDays, candlesDays: this.retention.candlesDays }, now);
|
|
524
|
+
res.ticksExpired = tk.ticks;
|
|
525
|
+
res.candlesExpired = tk.candles;
|
|
483
526
|
this.db.exec('COMMIT');
|
|
484
527
|
}
|
|
485
528
|
catch (err) {
|
|
486
529
|
this.db.exec('ROLLBACK');
|
|
487
530
|
throw err;
|
|
488
531
|
}
|
|
489
|
-
if (res.rawCleared || res.downsampled || res.expired)
|
|
490
|
-
this.log(`prune: raw ${res.rawCleared}, downsampled ${res.downsampled}, expired ${res.expired} rows`);
|
|
532
|
+
if (res.rawCleared || res.downsampled || res.expired || res.ticksExpired)
|
|
533
|
+
this.log(`prune: raw ${res.rawCleared}, downsampled ${res.downsampled}, expired ${res.expired} rows, ${res.ticksExpired} ticks`);
|
|
491
534
|
return res;
|
|
492
535
|
}
|
|
493
536
|
// ---------------------------------------------------------------- read side
|
|
@@ -512,7 +555,7 @@ export class Recorder {
|
|
|
512
555
|
const d = one("SELECT SUM(pgsize) AS b FROM dbstat WHERE name IN ('token_snapshots', 'token_snapshots_mint_t', 'token_snapshots_t', 'token_snapshots_t0_pending')");
|
|
513
556
|
if (d.b !== null && Number(s.n) > 0)
|
|
514
557
|
bytesPerSnapshot = Number(d.b) / Number(s.n);
|
|
515
|
-
const all = one("SELECT SUM(pgsize) AS b FROM dbstat WHERE name IN ('recorder_meta', 'feed_members', 'feed_members_mint_t', 'token_snapshots', 'token_snapshots_mint_t', 'token_snapshots_t', 'token_snapshots_t0_pending', 'mints', 'mints_last_live', 'sweeps', 'chain_events', 'chain_events_mint_t', 'outcomes', 'outcomes_label', 'verdicts', 'verdicts_mint_t')");
|
|
558
|
+
const all = one("SELECT SUM(pgsize) AS b FROM dbstat WHERE name IN ('recorder_meta', 'feed_members', 'feed_members_mint_t', 'token_snapshots', 'token_snapshots_mint_t', 'token_snapshots_t', 'token_snapshots_t0_pending', 'mints', 'mints_last_live', 'sweeps', 'chain_events', 'chain_events_mint_t', 'outcomes', 'outcomes_label', 'verdicts', 'verdicts_mint_t', 'creators', 'creators_creator', 'autotrade_ticks', 'autotrade_ticks_t', 'autotrade_candles_1m', 'autotrade_candles_1m_t')");
|
|
516
559
|
recorderBytes = all.b === null ? null : Number(all.b);
|
|
517
560
|
}
|
|
518
561
|
catch {
|
|
@@ -533,6 +576,9 @@ export class Recorder {
|
|
|
533
576
|
outcomesByLabel: byLabel,
|
|
534
577
|
sweeps: Number(one('SELECT COUNT(*) AS n FROM sweeps').n),
|
|
535
578
|
sweepCallsToday: this.sweepCallsToday(now),
|
|
579
|
+
ticks: Number(one('SELECT COUNT(*) AS n FROM autotrade_ticks').n),
|
|
580
|
+
candles: Number(one('SELECT COUNT(*) AS n FROM autotrade_candles_1m').n),
|
|
581
|
+
tickCallsToday: this.tickLane ? this.tickLane.callsToday(now) : tickCallsOn(this.db, now),
|
|
536
582
|
dbBytes, recorderBytes, bytesPerSnapshot,
|
|
537
583
|
oldestT: s.oldest === null ? null : Number(s.oldest),
|
|
538
584
|
newestT: s.newest === null ? null : Number(s.newest),
|
|
@@ -571,6 +617,17 @@ export class Recorder {
|
|
|
571
617
|
this.onLog?.(`recorder: ${line}\n`);
|
|
572
618
|
}
|
|
573
619
|
}
|
|
620
|
+
/** Tick calls the lane recorded for `now`'s UTC day (recorder_meta), for a reader with no lane of its own. */
|
|
621
|
+
function tickCallsOn(db, now) {
|
|
622
|
+
const r = db.prepare("SELECT value FROM recorder_meta WHERE key = 'tick_calls'").get();
|
|
623
|
+
try {
|
|
624
|
+
const v = r ? JSON.parse(r.value) : null;
|
|
625
|
+
return v && v.day === new Date(now).toISOString().slice(0, 10) ? Number(v.calls) || 0 : 0;
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
574
631
|
function msg(err) {
|
|
575
632
|
return err instanceof Error ? err.message : String(err);
|
|
576
633
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// dexterm history recorder: retention under a DB cap (task-1504, AUTOTRADE-SPEC §3 + §10 Q4).
|
|
2
|
+
//
|
|
3
|
+
// Before this, the collector's only answer to a full disk was to PAUSE observation above `--max-db-mb`, while the
|
|
4
|
+
// outcome sweeps kept writing: the cap stopped exactly the rows autotrade needs and the DB still grew past it.
|
|
5
|
+
// This module keeps the DB under the cap by EVICTING the oldest recorder history instead, so observation never
|
|
6
|
+
// pauses:
|
|
7
|
+
//
|
|
8
|
+
// age ticks after `ticksDays` (14), sampled candles after `candlesDays` (90). Snapshot / feed-member /
|
|
9
|
+
// sweep rows keep the recorder's existing `columnsDays` rule (recorder.ts prune; Q4's "prune raw rows
|
|
10
|
+
// older than 14 days" is `columnsDays: 14`).
|
|
11
|
+
// reclaim (optional) once the cap is reached, the host's cheap space goes first: the collector passes the data
|
|
12
|
+
// layer's own expired-cache prune (`Cache.prune()`, entries a day past their TTL), measured on
|
|
13
|
+
// 2026-10-21 at ~470 MB of a 1,173 MB file that only held ~430 MB of recorder history.
|
|
14
|
+
// cap once the DB's USED bytes (pages minus the freelist) reach `highWater` × `maxBytes`, delete the
|
|
15
|
+
// oldest slice of history (`sliceMs`, all recorder tables: ticks, candles, snapshots incl. sweep rows,
|
|
16
|
+
// feed members; the small sweep ledger keeps its age rule) and repeat until used ≤ `lowWater` × `maxBytes`. Rows newer than
|
|
17
|
+
// `keepMs` are never evicted. Outcomes, verdicts and creators are tiny and stay forever.
|
|
18
|
+
//
|
|
19
|
+
// SQLite does not shrink the file on DELETE: freed pages go to the freelist and new rows reuse them. So the file
|
|
20
|
+
// stops growing at about the high-water mark; a file that is ALREADY above the cap stays that size (only
|
|
21
|
+
// `VACUUM`, with the collector stopped, gives the space back) but no longer grows.
|
|
22
|
+
export const DEFAULT_CAP = { maxBytes: null, highWater: 0.95, lowWater: 0.85, sliceMs: 3_600_000, keepMs: 3_600_000, maxSlices: 500 };
|
|
23
|
+
export const DEFAULT_AGE = { ticksDays: 14, candlesDays: 90 };
|
|
24
|
+
/** The Q4 default cap when retention-by-cap is switched on (3 GB). */
|
|
25
|
+
export const DEFAULT_CAP_BYTES = 3_000_000_000;
|
|
26
|
+
/** Tables the cap may evict from, each with its time column. */
|
|
27
|
+
const EVICTABLE = [
|
|
28
|
+
{ table: 'autotrade_ticks', col: 't' },
|
|
29
|
+
{ table: 'autotrade_candles_1m', col: 't' },
|
|
30
|
+
{ table: 'token_snapshots', col: 't' },
|
|
31
|
+
{ table: 'feed_members', col: 't' },
|
|
32
|
+
];
|
|
33
|
+
export function dbSize(db) {
|
|
34
|
+
const one = (sql) => Number(Object.values(db.prepare(sql).get())[0]);
|
|
35
|
+
const ps = one('PRAGMA page_size');
|
|
36
|
+
const pc = one('PRAGMA page_count');
|
|
37
|
+
const fl = one('PRAGMA freelist_count');
|
|
38
|
+
return { fileBytes: pc * ps, usedBytes: (pc - fl) * ps };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Enforce the cap: a no-op below the high-water mark; above it, delete the oldest `sliceMs` of history across every
|
|
42
|
+
* evictable table, one transaction per slice, until used ≤ the low-water mark. Never deletes rows newer than
|
|
43
|
+
* `now - keepMs`. Outcome / verdict / creator rows are never touched.
|
|
44
|
+
*/
|
|
45
|
+
export function enforceCap(db, policy, now) {
|
|
46
|
+
const p = { ...DEFAULT_CAP, ...policy };
|
|
47
|
+
const size0 = dbSize(db);
|
|
48
|
+
const res = { evicted: false, usedBefore: size0.usedBytes, usedAfter: size0.usedBytes, fileBytes: size0.fileBytes, slices: 0, reclaimed: 0, deleted: {}, cutoff: null, over: false };
|
|
49
|
+
if (p.maxBytes === null || p.maxBytes <= 0 || size0.usedBytes < p.highWater * p.maxBytes)
|
|
50
|
+
return res;
|
|
51
|
+
res.evicted = true;
|
|
52
|
+
const target = p.lowWater * p.maxBytes;
|
|
53
|
+
const floor = now - p.keepMs;
|
|
54
|
+
const tables = EVICTABLE.filter((e) => !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(e.table));
|
|
55
|
+
const oldestQ = tables.map((e) => ({ ...e, q: db.prepare(`SELECT MIN(${e.col}) AS m FROM ${e.table}`), del: db.prepare(`DELETE FROM ${e.table} WHERE ${e.col} < ?`) }));
|
|
56
|
+
const delMints = db.prepare('DELETE FROM mints WHERE last_any_t < ?');
|
|
57
|
+
let used = size0.usedBytes;
|
|
58
|
+
if (p.reclaim) {
|
|
59
|
+
res.reclaimed = p.reclaim();
|
|
60
|
+
used = dbSize(db).usedBytes;
|
|
61
|
+
}
|
|
62
|
+
while (used > target && res.slices < p.maxSlices) {
|
|
63
|
+
let oldest = Infinity;
|
|
64
|
+
for (const e of oldestQ) {
|
|
65
|
+
const m = e.q.get().m;
|
|
66
|
+
if (m !== null && Number(m) < oldest)
|
|
67
|
+
oldest = Number(m);
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isFinite(oldest) || oldest >= floor)
|
|
70
|
+
break;
|
|
71
|
+
const cutoff = Math.min(oldest + p.sliceMs, floor);
|
|
72
|
+
db.exec('BEGIN');
|
|
73
|
+
try {
|
|
74
|
+
for (const e of oldestQ)
|
|
75
|
+
res.deleted[e.table] = (res.deleted[e.table] ?? 0) + Number(e.del.run(cutoff).changes);
|
|
76
|
+
res.deleted.mints = (res.deleted.mints ?? 0) + Number(delMints.run(cutoff).changes);
|
|
77
|
+
db.exec('COMMIT');
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
db.exec('ROLLBACK');
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
res.slices++;
|
|
84
|
+
res.cutoff = cutoff;
|
|
85
|
+
used = dbSize(db).usedBytes;
|
|
86
|
+
}
|
|
87
|
+
const size1 = dbSize(db);
|
|
88
|
+
res.usedAfter = size1.usedBytes;
|
|
89
|
+
res.fileBytes = size1.fileBytes;
|
|
90
|
+
res.over = size1.usedBytes >= p.highWater * p.maxBytes;
|
|
91
|
+
return res;
|
|
92
|
+
}
|
|
93
|
+
/** Age rules for the autotrade tables (the recorder's prune() calls this next to its own snapshot rules). */
|
|
94
|
+
export function pruneTicksByAge(db, policy, now) {
|
|
95
|
+
const p = { ...DEFAULT_AGE, ...policy };
|
|
96
|
+
const day = 86_400_000;
|
|
97
|
+
const ticks = Number(db.prepare('DELETE FROM autotrade_ticks WHERE t < ?').run(now - p.ticksDays * day).changes);
|
|
98
|
+
const candles = Number(db.prepare('DELETE FROM autotrade_candles_1m WHERE t < ?').run(now - p.candlesDays * day).changes);
|
|
99
|
+
return { ticks, candles };
|
|
100
|
+
}
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
// - `mints` is a per-mint summary the sweep scheduler reads instead of scanning snapshots.
|
|
13
13
|
// - feed_members is keyed on (t, feed, rank): the refresh tick is the run.
|
|
14
14
|
// - chain_events and verdicts are created here (one schema owner) but written by R-02 / L-01.
|
|
15
|
-
|
|
15
|
+
//
|
|
16
|
+
// v2 (task-1504, AUTOTRADE-SPEC §3): `autotrade_ticks` (position-grade samples every 5–10 s for the few coins
|
|
17
|
+
// that are watched, open, a candidate or held; written by the tick lane in ticks.ts) and `autotrade_candles_1m`
|
|
18
|
+
// (1-minute OHLC built from those samples). Both are additive: a v1 file gains the two tables on open.
|
|
19
|
+
export const SCHEMA_VERSION = 2;
|
|
16
20
|
export const HORIZONS_SEC = [3600, 7200, 21600, 86400, 259200];
|
|
17
21
|
/** Every horizon labelled: the bitmask value of `horizons_done` for a finished t0 row. */
|
|
18
22
|
export const ALL_HORIZONS_MASK = (1 << HORIZONS_SEC.length) - 1;
|
|
@@ -143,4 +147,33 @@ CREATE TABLE IF NOT EXISTS creators (
|
|
|
143
147
|
created_at INTEGER -- the coin's creation time when the source gave one
|
|
144
148
|
) WITHOUT ROWID;
|
|
145
149
|
CREATE INDEX IF NOT EXISTS creators_creator ON creators (creator, first_t);
|
|
150
|
+
|
|
151
|
+
-- task-1504 (AUTOTRADE-SPEC §3): position-grade ticks. One row per (mint, poll) for focus coins, written
|
|
152
|
+
-- every sample (no change detection), from DexScreener tokens/v1 batches. price_usd NULL = the poll got no
|
|
153
|
+
-- pair back for the mint (pool gone or not indexed): a held coin's rug shows up here as NULL rows.
|
|
154
|
+
CREATE TABLE IF NOT EXISTS autotrade_ticks (
|
|
155
|
+
mint TEXT NOT NULL,
|
|
156
|
+
t INTEGER NOT NULL, -- unix ms the answer was fetched
|
|
157
|
+
role TEXT NOT NULL, -- why it was polled: 'held' | 'open' | 'candidate' | 'watch'
|
|
158
|
+
price_usd REAL, liquidity_usd REAL, mcap REAL, fdv REAL,
|
|
159
|
+
vol_m5 REAL, vol_h1 REAL,
|
|
160
|
+
buys_m5 INTEGER, sells_m5 INTEGER,
|
|
161
|
+
chg_m5 REAL, chg_h1 REAL,
|
|
162
|
+
pair_address TEXT,
|
|
163
|
+
pair_created_at INTEGER,
|
|
164
|
+
PRIMARY KEY (mint, t)
|
|
165
|
+
) WITHOUT ROWID;
|
|
166
|
+
CREATE INDEX IF NOT EXISTS autotrade_ticks_t ON autotrade_ticks (t);
|
|
167
|
+
|
|
168
|
+
-- 1-minute candles for the same coins, built from the ticks (sampled: high/low are of the 5–10 s samples,
|
|
169
|
+
-- not of every trade). t = the minute's start; n = samples in the minute.
|
|
170
|
+
CREATE TABLE IF NOT EXISTS autotrade_candles_1m (
|
|
171
|
+
mint TEXT NOT NULL,
|
|
172
|
+
t INTEGER NOT NULL,
|
|
173
|
+
o REAL NOT NULL, h REAL NOT NULL, l REAL NOT NULL, c REAL NOT NULL,
|
|
174
|
+
liq_c REAL, mcap_c REAL,
|
|
175
|
+
n INTEGER NOT NULL,
|
|
176
|
+
PRIMARY KEY (mint, t)
|
|
177
|
+
) WITHOUT ROWID;
|
|
178
|
+
CREATE INDEX IF NOT EXISTS autotrade_candles_1m_t ON autotrade_candles_1m (t);
|
|
146
179
|
`;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// dexterm history recorder: position-grade ticks for autotrade (task-1504, AUTOTRADE-SPEC §3 "Data today vs what
|
|
2
|
+
// must be recorded"). The change-driven `token_snapshots` rows land every ~2 min at best and only on a ≥ 5 % move,
|
|
3
|
+
// which hides the intrabar path a stop or a take-profit lives on. This lane samples the FEW coins that matter to a
|
|
4
|
+
// position every 5–10 s and writes every sample:
|
|
5
|
+
//
|
|
6
|
+
// focus held > open > candidate > watch, one role per mint (the highest wins), at most `maxCoins`
|
|
7
|
+
// (default 60 = two calls). Sources are injected: the collector reads the watchlist and the paper
|
|
8
|
+
// wallet from the DB and the autotrade focus file (`focus.json`, written by the runner / the app).
|
|
9
|
+
// cadence held + open coins every `fastEveryMs` (5 s), candidate + watch every `slowEveryMs` (10 s). A call
|
|
10
|
+
// that goes out anyway is topped up with coins not yet due (oldest sample first): a batch of 30
|
|
11
|
+
// costs the same one request.
|
|
12
|
+
// budget DexScreener `tokens/v1` batches of 30 only (never a per-coin keyless Jupiter call), through the
|
|
13
|
+
// data layer's limiter and health gate; 'normal' priority when a held/open coin is in the call,
|
|
14
|
+
// 'low' otherwise; skipped while the bucket is paused after a 429; a per-UTC-day call ceiling.
|
|
15
|
+
// candles every sample with a price folds into `autotrade_candles_1m` (sampled OHLC: high/low are of the
|
|
16
|
+
// samples, not of every trade).
|
|
17
|
+
//
|
|
18
|
+
// No change detection and no raw JSON: a tick row is ~100 bytes. Retention (retention.ts) keeps ticks 14 days.
|
|
19
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import { withPriority, systemClock } from '../../data/src/index.js';
|
|
22
|
+
/** Priority order: earlier roles are sampled first and faster. */
|
|
23
|
+
export const FOCUS_ROLES = ['held', 'open', 'candidate', 'watch'];
|
|
24
|
+
export const DEFAULT_TICKS = { fastEveryMs: 5_000, slowEveryMs: 10_000, maxCoins: 60, batch: 30, maxCallsPerDay: 36_000 };
|
|
25
|
+
const BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
26
|
+
const MIN = 60_000;
|
|
27
|
+
const fin = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
28
|
+
const isFast = (r) => r === 'held' || r === 'open';
|
|
29
|
+
/** One role per mint (the highest-priority role wins), invalid mints dropped, in priority order. */
|
|
30
|
+
export function mergeFocus(entries) {
|
|
31
|
+
const best = new Map();
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
if (!e || typeof e.mint !== 'string' || !BASE58.test(e.mint) || !FOCUS_ROLES.includes(e.role))
|
|
34
|
+
continue;
|
|
35
|
+
const cur = best.get(e.mint);
|
|
36
|
+
if (!cur || FOCUS_ROLES.indexOf(e.role) < FOCUS_ROLES.indexOf(cur))
|
|
37
|
+
best.set(e.mint, e.role);
|
|
38
|
+
}
|
|
39
|
+
return [...best].map(([mint, role]) => ({ mint, role })).sort((a, b) => FOCUS_ROLES.indexOf(a.role) - FOCUS_ROLES.indexOf(b.role));
|
|
40
|
+
}
|
|
41
|
+
/** The sampler. Owns no connection: it writes through the recorder's DB handle (same file, same thread). */
|
|
42
|
+
export class TickLane {
|
|
43
|
+
policy;
|
|
44
|
+
db;
|
|
45
|
+
focus;
|
|
46
|
+
clock;
|
|
47
|
+
onLog;
|
|
48
|
+
/** mint -> time of its last sample this process took. */
|
|
49
|
+
last = new Map();
|
|
50
|
+
polling = false;
|
|
51
|
+
day = '';
|
|
52
|
+
dayCalls = 0;
|
|
53
|
+
counters = { polls: 0, calls: 0, written: 0 };
|
|
54
|
+
stmts;
|
|
55
|
+
constructor(db, opts) {
|
|
56
|
+
this.db = db;
|
|
57
|
+
this.focus = opts.focus;
|
|
58
|
+
this.policy = { ...DEFAULT_TICKS, ...opts.policy };
|
|
59
|
+
this.clock = opts.clock ?? systemClock;
|
|
60
|
+
this.onLog = opts.onLog;
|
|
61
|
+
this.stmts = {
|
|
62
|
+
tick: db.prepare(`INSERT OR IGNORE INTO autotrade_ticks (mint, t, role, price_usd, liquidity_usd, mcap, fdv, vol_m5, vol_h1, buys_m5, sells_m5, chg_m5, chg_h1, pair_address, pair_created_at)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
64
|
+
candle: db.prepare(`INSERT INTO autotrade_candles_1m (mint, t, o, h, l, c, liq_c, mcap_c, n) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
65
|
+
ON CONFLICT(mint, t) DO UPDATE SET h = MAX(h, excluded.h), l = MIN(l, excluded.l), c = excluded.c, liq_c = excluded.liq_c, mcap_c = excluded.mcap_c, n = n + 1`),
|
|
66
|
+
meta: db.prepare("INSERT OR REPLACE INTO recorder_meta (key, value) VALUES ('tick_calls', ?)"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Tick calls issued in the current UTC day (persisted in recorder_meta, so a restart keeps the count). */
|
|
70
|
+
callsToday(now = this.clock.now()) {
|
|
71
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
72
|
+
if (day !== this.day) {
|
|
73
|
+
this.day = day;
|
|
74
|
+
this.dayCalls = 0;
|
|
75
|
+
const r = this.db.prepare("SELECT value FROM recorder_meta WHERE key = 'tick_calls'").get();
|
|
76
|
+
try {
|
|
77
|
+
const v = r ? JSON.parse(r.value) : null;
|
|
78
|
+
if (v && v.day === day)
|
|
79
|
+
this.dayCalls = Number(v.calls) || 0;
|
|
80
|
+
}
|
|
81
|
+
catch { /* reset */ }
|
|
82
|
+
}
|
|
83
|
+
return this.dayCalls;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Which coins this poll samples: every due coin in priority order up to `maxCoins`, then the last batch topped
|
|
87
|
+
* up with coins not yet due (oldest sample first). `overCap` counts focus coins past `maxCoins`.
|
|
88
|
+
*/
|
|
89
|
+
plan(now = this.clock.now(), maxCalls = Math.ceil(this.policy.maxCoins / this.policy.batch)) {
|
|
90
|
+
const focus = mergeFocus(this.focus());
|
|
91
|
+
const cap = Math.min(this.policy.maxCoins, maxCalls * this.policy.batch);
|
|
92
|
+
const inCap = focus.slice(0, cap);
|
|
93
|
+
const overCap = focus.length - inCap.length;
|
|
94
|
+
const isDue = (e) => now - (this.last.get(e.mint) ?? -Infinity) >= (isFast(e.role) ? this.policy.fastEveryMs : this.policy.slowEveryMs);
|
|
95
|
+
const due = inCap.filter(isDue);
|
|
96
|
+
if (!due.length)
|
|
97
|
+
return { coins: [], due: 0, overCap, focus: focus.length };
|
|
98
|
+
const room = Math.ceil(due.length / this.policy.batch) * this.policy.batch - due.length;
|
|
99
|
+
const ride = inCap.filter((e) => !isDue(e)).sort((a, b) => (this.last.get(a.mint) ?? 0) - (this.last.get(b.mint) ?? 0)).slice(0, room);
|
|
100
|
+
return { coins: [...due, ...ride], due: due.length, overCap, focus: focus.length };
|
|
101
|
+
}
|
|
102
|
+
/** One poll. The host calls this every `fastEveryMs` (the collector's tick loop). Never throws. */
|
|
103
|
+
async poll(deps, now = this.clock.now()) {
|
|
104
|
+
const none = (skipped, overCap = 0) => ({ coins: 0, calls: 0, answered: 0, written: 0, overCap, skipped });
|
|
105
|
+
if (this.polling)
|
|
106
|
+
return none('in-flight');
|
|
107
|
+
let budget = this.policy.maxCallsPerDay - this.callsToday(now);
|
|
108
|
+
if (budget <= 0)
|
|
109
|
+
return none('day-cap');
|
|
110
|
+
let planned;
|
|
111
|
+
try {
|
|
112
|
+
planned = this.plan(now, Math.min(budget, Math.ceil(this.policy.maxCoins / this.policy.batch)));
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.log(`focus failed: ${msg(err)}`);
|
|
116
|
+
return none('no-focus');
|
|
117
|
+
}
|
|
118
|
+
if (!planned.coins.length)
|
|
119
|
+
return none(planned.focus ? 'not-due' : 'no-focus', planned.overCap);
|
|
120
|
+
if (deps.limiter.bucket('dexscreener').stats().pausedUntil > now)
|
|
121
|
+
return none('paused', planned.overCap);
|
|
122
|
+
this.polling = true;
|
|
123
|
+
this.counters.polls++;
|
|
124
|
+
const res = { coins: planned.coins.length, calls: 0, answered: 0, written: 0, overCap: planned.overCap, skipped: null };
|
|
125
|
+
try {
|
|
126
|
+
for (let i = 0; i < planned.coins.length && budget > 0; i += this.policy.batch) {
|
|
127
|
+
const chunk = planned.coins.slice(i, i + this.policy.batch);
|
|
128
|
+
const priority = chunk.some((e) => isFast(e.role)) ? 'normal' : 'low';
|
|
129
|
+
res.calls++;
|
|
130
|
+
budget--;
|
|
131
|
+
this.dayCalls++;
|
|
132
|
+
this.stmts.meta.run(JSON.stringify({ day: this.day, calls: this.dayCalls }));
|
|
133
|
+
const views = await withPriority(priority, () => deps.dexscreener.tokens(chunk.map((e) => e.mint)));
|
|
134
|
+
const t = this.clock.now();
|
|
135
|
+
const w = this.write(chunk, views, t);
|
|
136
|
+
res.answered += w.answered;
|
|
137
|
+
res.written += w.written;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
res.error = msg(err);
|
|
142
|
+
this.log(`tick poll failed: ${res.error}`);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
this.polling = false;
|
|
146
|
+
this.counters.calls += res.calls;
|
|
147
|
+
this.counters.written += res.written;
|
|
148
|
+
}
|
|
149
|
+
return res;
|
|
150
|
+
}
|
|
151
|
+
/** Write one answered batch (one transaction): a tick per coin (NULL price when no pair came back) + candles. */
|
|
152
|
+
write(chunk, views, t) {
|
|
153
|
+
let answered = 0, written = 0;
|
|
154
|
+
this.db.exec('BEGIN');
|
|
155
|
+
try {
|
|
156
|
+
for (const e of chunk) {
|
|
157
|
+
this.last.set(e.mint, t);
|
|
158
|
+
const p = views.get(e.mint)?.pair;
|
|
159
|
+
if (p)
|
|
160
|
+
answered++;
|
|
161
|
+
const price = p ? fin(p.priceUsd) : null;
|
|
162
|
+
const liq = p ? fin(p.liquidityUsd) : null;
|
|
163
|
+
const mcap = p ? fin(p.mcap) : null;
|
|
164
|
+
const r = this.stmts.tick.run(e.mint, t, e.role, price, liq, mcap, p ? fin(p.fdv) : null, p ? fin(p.volume.m5) : null, p ? fin(p.volume.h1) : null, p ? fin(p.txns.m5?.buys) : null, p ? fin(p.txns.m5?.sells) : null, p ? fin(p.priceChange.m5) : null, p ? fin(p.priceChange.h1) : null, p?.pairAddress || null, p ? fin(p.createdAt) : null);
|
|
165
|
+
if (Number(r.changes) > 0)
|
|
166
|
+
written++;
|
|
167
|
+
if (price !== null && price > 0 && Number(r.changes) > 0)
|
|
168
|
+
this.stmts.candle.run(e.mint, Math.floor(t / MIN) * MIN, price, price, price, price, liq, mcap);
|
|
169
|
+
}
|
|
170
|
+
this.db.exec('COMMIT');
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
this.db.exec('ROLLBACK');
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
// Forget coins that left the focus long ago (bounded memory over a long run).
|
|
177
|
+
if (this.last.size > 4 * this.policy.maxCoins)
|
|
178
|
+
for (const [m, lt] of this.last)
|
|
179
|
+
if (t - lt > 3_600_000)
|
|
180
|
+
this.last.delete(m);
|
|
181
|
+
return { answered, written };
|
|
182
|
+
}
|
|
183
|
+
log(line) {
|
|
184
|
+
this.onLog?.(`recorder: ${line}\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------- focus sources
|
|
188
|
+
/**
|
|
189
|
+
* Focus coins the app's own DB already knows: every watchlist mint ('watch') and every paper position with a
|
|
190
|
+
* token balance ('held'). Tables that do not exist yet (a fresh DB) contribute nothing.
|
|
191
|
+
*/
|
|
192
|
+
export function dbFocus(db) {
|
|
193
|
+
const has = (name) => !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
|
|
194
|
+
const out = [];
|
|
195
|
+
if (has('watchlist'))
|
|
196
|
+
for (const r of db.prepare('SELECT mint FROM watchlist').all())
|
|
197
|
+
out.push({ mint: r.mint, role: 'watch' });
|
|
198
|
+
if (has('paper_positions'))
|
|
199
|
+
for (const r of db.prepare("SELECT mint FROM paper_positions WHERE tokens_raw NOT IN ('0', '')").all())
|
|
200
|
+
out.push({ mint: r.mint, role: 'held' });
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
/** The rendezvous file other processes (the autotrade runner, the app) publish focus coins in. */
|
|
204
|
+
export const FOCUS_FILE_SCHEMA = 'dexterm-autotrade-focus/1';
|
|
205
|
+
/** A focus file not rewritten for this long is ignored (a dead writer must not pin coins to the fast lane). */
|
|
206
|
+
export const FOCUS_FILE_STALE_MS = 30 * MIN;
|
|
207
|
+
/** `<dexterm home>/autotrade/focus.json`. */
|
|
208
|
+
export const focusFilePath = (home) => join(home, 'autotrade', 'focus.json');
|
|
209
|
+
/** Read a focus file: missing, garbled, stale or expired entries contribute nothing. Never throws. */
|
|
210
|
+
export function readFocusFile(path, now) {
|
|
211
|
+
let doc;
|
|
212
|
+
try {
|
|
213
|
+
doc = JSON.parse(readFileSync(path, 'utf8'));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
if (!doc || doc.schema !== FOCUS_FILE_SCHEMA || !Array.isArray(doc.entries) || !(now - Number(doc.updatedAt) <= FOCUS_FILE_STALE_MS))
|
|
219
|
+
return [];
|
|
220
|
+
return mergeFocus(doc.entries.filter((e) => e && (e.until === undefined || Number(e.until) >= now)).map((e) => ({ mint: e.mint, role: e.role })));
|
|
221
|
+
}
|
|
222
|
+
/** Write a focus file atomically (tmp + rename, mode 0600). */
|
|
223
|
+
export function writeFocusFile(path, entries, now) {
|
|
224
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
225
|
+
const doc = { schema: FOCUS_FILE_SCHEMA, updatedAt: now, entries: [...entries] };
|
|
226
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
227
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\n', { mode: 0o600 });
|
|
228
|
+
renameSync(tmp, path);
|
|
229
|
+
}
|
|
230
|
+
/** Sampled 1m candles for one mint, oldest first (read-only use). */
|
|
231
|
+
export function candlesFromDb(db, mint, from = 0, to = Number.MAX_SAFE_INTEGER) {
|
|
232
|
+
return db.prepare('SELECT t, o, h, l, c, liq_c, mcap_c, n FROM autotrade_candles_1m WHERE mint = ? AND t >= ? AND t <= ? ORDER BY t').all(mint, from, to)
|
|
233
|
+
.map((r) => ({ t: Number(r.t), o: Number(r.o), h: Number(r.h), l: Number(r.l), c: Number(r.c), liq: r.liq_c ?? null, mcap: r.mcap_c ?? null, n: Number(r.n) }));
|
|
234
|
+
}
|
|
235
|
+
function msg(err) {
|
|
236
|
+
return err instanceof Error ? err.message : String(err);
|
|
237
|
+
}
|
|
@@ -66,6 +66,11 @@ export class Canvas {
|
|
|
66
66
|
*/
|
|
67
67
|
images;
|
|
68
68
|
imageMode;
|
|
69
|
+
/**
|
|
70
|
+
* task-1466: where each keyed list row was drawn (a table with `rowKey`, e.g. a screener row by mint), so an
|
|
71
|
+
* overlay drawn later (the agent HUD's row glow / cursor) can find the row on screen. Not emitted anywhere.
|
|
72
|
+
*/
|
|
73
|
+
rowMarks;
|
|
69
74
|
constructor(w, h, theme) {
|
|
70
75
|
this.w = w;
|
|
71
76
|
this.h = h;
|
|
@@ -82,6 +87,7 @@ export class Canvas {
|
|
|
82
87
|
this.shields = [];
|
|
83
88
|
this.images = [];
|
|
84
89
|
this.imageMode = 'cells';
|
|
90
|
+
this.rowMarks = [];
|
|
85
91
|
}
|
|
86
92
|
idx(x, y) { return y * this.w + x; }
|
|
87
93
|
/** Copy every buffer of `src` (same size) into this canvas. */
|
|
@@ -97,6 +103,7 @@ export class Canvas {
|
|
|
97
103
|
this.shields = src.shields.slice();
|
|
98
104
|
this.images = src.images.slice();
|
|
99
105
|
this.imageMode = src.imageMode;
|
|
106
|
+
this.rowMarks = src.rowMarks.slice();
|
|
100
107
|
}
|
|
101
108
|
put(x, y, ch, st = {}) {
|
|
102
109
|
if (x < 0 || y < 0 || x >= this.w || y >= this.h)
|
|
@@ -119,6 +119,59 @@ export function fmtMcap(n) {
|
|
|
119
119
|
return bare;
|
|
120
120
|
return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* task-1472: THE chart number formatter (Y axis, crosshair/legend, last-value tag, volume): compact with a LOWERCASE k
|
|
124
|
+
* for thousands, then M, B, T. One decimal while the value is below 10 of its unit, none after, trailing `.0` dropped:
|
|
125
|
+
* 4930 → 4.9k, 42100 → 42k, 487000 → 487k, 1.23e6 → 1.2M, 4.8e7 → 48M, 1.3e9 → 1.3B, 999 → 999. The unit is promoted
|
|
126
|
+
* AFTER rounding, so 999 900 reads 1M (never 1000k). Below 1 it is the subscript price form (0.0000123 → 0.0₄123):
|
|
127
|
+
* never scientific notation, never a long run of zeros. `extra` (0..3) adds decimals when an axis needs them to keep
|
|
128
|
+
* two neighbouring ticks apart (see compactDecimalsFor). No currency symbol; fmtCompactUsd adds the `$`.
|
|
129
|
+
*/
|
|
130
|
+
export function fmtCompact(n, extra = 0) {
|
|
131
|
+
if (n == null || !Number.isFinite(n))
|
|
132
|
+
return '—';
|
|
133
|
+
if (n === 0)
|
|
134
|
+
return '0';
|
|
135
|
+
const sign = n < 0 ? '-' : '';
|
|
136
|
+
const a = Math.abs(n);
|
|
137
|
+
if (a < 1)
|
|
138
|
+
return sign + trimZeros(fmtPriceBare(a));
|
|
139
|
+
const units = ['', 'k', 'M', 'B', 'T'];
|
|
140
|
+
const x = Math.max(0, Math.min(3, Math.floor(extra)));
|
|
141
|
+
const round = (q) => Number(q.toFixed((q < 10 ? 1 : 0) + x));
|
|
142
|
+
let u = 0, v = a;
|
|
143
|
+
while (u < units.length - 1 && v >= 1000) {
|
|
144
|
+
v /= 1000;
|
|
145
|
+
u++;
|
|
146
|
+
}
|
|
147
|
+
let r = round(v);
|
|
148
|
+
if (r >= 1000 && u < units.length - 1) {
|
|
149
|
+
u++;
|
|
150
|
+
r = round(r / 1000);
|
|
151
|
+
}
|
|
152
|
+
return `${sign}${trimZeros(r.toFixed((r < 10 ? 1 : 0) + x))}${units[u]}`;
|
|
153
|
+
}
|
|
154
|
+
/** USD form of fmtCompact: `$4.9k`, `$48M`, `-$1.2k`. */
|
|
155
|
+
export function fmtCompactUsd(n, extra = 0) {
|
|
156
|
+
const bare = fmtCompact(n, extra);
|
|
157
|
+
if (bare === '—')
|
|
158
|
+
return bare;
|
|
159
|
+
return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* task-1472: the extra decimals (0..3) fmtCompact needs so that axis ticks `step` apart around `around` never print
|
|
163
|
+
* the same label (a 48.1M–48.6M range would otherwise read 48M on every tick). 0 for a normal memecoin range.
|
|
164
|
+
*/
|
|
165
|
+
export function compactDecimalsFor(around, step) {
|
|
166
|
+
if (!Number.isFinite(around) || !Number.isFinite(step) || step <= 0 || Math.abs(around) < 1)
|
|
167
|
+
return 0;
|
|
168
|
+
for (let x = 0; x < 3; x++) {
|
|
169
|
+
const a = fmtCompact(around, x);
|
|
170
|
+
if (a !== fmtCompact(around + step, x) && a !== fmtCompact(around - step, x))
|
|
171
|
+
return x;
|
|
172
|
+
}
|
|
173
|
+
return 3;
|
|
174
|
+
}
|
|
122
175
|
export function fmtPct(n, { sign = true, digits = 1 } = {}) {
|
|
123
176
|
if (n == null || Number.isNaN(n))
|
|
124
177
|
return '—';
|