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
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
// dexterm — AUTOTRADE (paper) screen renderer (`Q`, task-1508, AUTOTRADE-SPEC §2b): strategies, editor, compare,
|
|
2
|
+
// backtest and runner views over AutotradeUi (autotrade-ui.ts). Read-only drawing; the verbs live in
|
|
3
|
+
// autotrade-keys.ts. Every view says PAPER; no view offers live mode, a wallet, a key or the signer.
|
|
4
|
+
import { Canvas, header, footer, breakpoint, fmtAgeSec, sparkline, wrap } from '../../../tui/src/index.js';
|
|
5
|
+
import { compareRows as fleetCompareRows, compareTable as fleetCompareTable, fleetTotals } from '../../../autotrade/src/index.js';
|
|
6
|
+
import { tokenize } from "../screener/filter.js";
|
|
7
|
+
import { headerModel } from "../state.js";
|
|
8
|
+
import { AUTOTRADE_VIEWS, VIEW_TITLE, READ_ONLY_PATHS, backtestSubject, compareRows, getAt, heartbeatStale, issuesByField, screenHighlight, } from "./autotrade-ui.js";
|
|
9
|
+
/** Per-screen memo of file reads + validation, keyed by what the list says about a strategy (id, hash, version,
|
|
10
|
+
* last backtest): a frame never re-reads a strategy file; a save, import or backtest reloads the list and so the key. */
|
|
11
|
+
const MEMO = new WeakMap();
|
|
12
|
+
function memo(ui, key, f) {
|
|
13
|
+
let m = MEMO.get(ui);
|
|
14
|
+
if (!m) {
|
|
15
|
+
m = new Map();
|
|
16
|
+
MEMO.set(ui, m);
|
|
17
|
+
}
|
|
18
|
+
if (!m.has(key)) {
|
|
19
|
+
if (m.size > 32)
|
|
20
|
+
m.clear();
|
|
21
|
+
m.set(key, f());
|
|
22
|
+
}
|
|
23
|
+
return m.get(key);
|
|
24
|
+
}
|
|
25
|
+
const entryKey = (ui, id) => { const e = ui.entries.find((x) => x.id === id); return e ? `${e.id}|${e.source}|${e.hash ?? ''}|${e.version ?? ''}|${e.lastBacktest?.t ?? ''}|${e.ok}` : `${id}|missing`; };
|
|
26
|
+
function fit(s, w) { return w <= 0 ? '' : s.length > w ? (w > 1 ? s.slice(0, w - 1) + '…' : s.slice(0, w)) : s; }
|
|
27
|
+
const usd = (x) => `${x >= 0 ? '+' : '-'}$${Math.abs(x).toFixed(2)}`;
|
|
28
|
+
const pct = (x, dp = 1) => (x === null || x === undefined ? '—' : `${x >= 0 ? '+' : ''}${x.toFixed(dp)}%`);
|
|
29
|
+
export function renderAutotrade(c, state) {
|
|
30
|
+
const t = c.theme;
|
|
31
|
+
header(c, headerModel(state));
|
|
32
|
+
const ui = state.autotradeUi ?? null;
|
|
33
|
+
if (!ui) {
|
|
34
|
+
c.text(1, 3, 'autotrade is not loaded', { fg: t.dim, bg: t.bg });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const now = ui.deps.now();
|
|
38
|
+
tabs(c, ui);
|
|
39
|
+
const top = 3, bottom = c.h - 1;
|
|
40
|
+
const area = { x: 0, y: top, w: c.w, h: bottom - top };
|
|
41
|
+
if (ui.view === 'list')
|
|
42
|
+
renderList(c, ui, area, now);
|
|
43
|
+
else if (ui.view === 'editor')
|
|
44
|
+
renderEditor(c, ui, area);
|
|
45
|
+
else if (ui.view === 'compare')
|
|
46
|
+
renderCompare(c, ui, area);
|
|
47
|
+
else if (ui.view === 'backtest')
|
|
48
|
+
renderBacktest(c, ui, area, now);
|
|
49
|
+
else
|
|
50
|
+
renderRunner(c, ui, area, now);
|
|
51
|
+
if (ui.confirm)
|
|
52
|
+
confirmCard(c);
|
|
53
|
+
if (ui.review)
|
|
54
|
+
reviewCard(c, ui);
|
|
55
|
+
footer(c, hintsFor(ui), t.name);
|
|
56
|
+
}
|
|
57
|
+
/** Row 2: PAPER badge + the five views (the active one lit) + the notice / input line on the right. */
|
|
58
|
+
function tabs(c, ui) {
|
|
59
|
+
const t = c.theme;
|
|
60
|
+
const y = 2;
|
|
61
|
+
c.fill(0, y, c.w, 1, ' ', { bg: t.bg });
|
|
62
|
+
// task-1528: a live run (through the live signer) is labelled LIVE (mainnet, red) or DEVNET; everything else PAPER.
|
|
63
|
+
const mode = runMode(ui);
|
|
64
|
+
let x = c.text(1, y, mode === 'live' ? ' LIVE · REAL FUNDS ' : mode === 'devnet' ? ' DEVNET ' : ' PAPER ', { fg: t.bg, bg: mode === 'live' ? t.down : t.warn, b: true });
|
|
65
|
+
x = c.text(x + 1, y, 'AUTOTRADE', { fg: t.accent, bg: t.bg, b: true }) + 2;
|
|
66
|
+
for (const v of AUTOTRADE_VIEWS) {
|
|
67
|
+
const on = ui.view === v;
|
|
68
|
+
const off = (v === 'compare' && !ui.compare) || (v === 'editor' && !ui.editor);
|
|
69
|
+
x = c.text(x, y, ` ${VIEW_TITLE[v]} `, { fg: on ? t.text : off ? t.dim : t.muted, bg: on ? t.tabActiveBg : t.bg, b: on }) + 1;
|
|
70
|
+
}
|
|
71
|
+
const line = ui.input
|
|
72
|
+
? `${ui.input.kind === 'import' ? 'import file: ' : ui.input.kind === 'dup' ? `duplicate ${ui.input.from} as: ` : `new from ${ui.input.from} as: `}${ui.input.text}█ ⏎ ok · esc cancel`
|
|
73
|
+
: ui.notice;
|
|
74
|
+
if (line && x < c.w - 10)
|
|
75
|
+
c.text(x + 1, y, fit(line, c.w - x - 2), { fg: ui.input ? t.text : t.warn, bg: t.bg, i: !ui.input });
|
|
76
|
+
}
|
|
77
|
+
function hintsFor(ui) {
|
|
78
|
+
const common = [['esc', 'back'], ['q', 'quit']];
|
|
79
|
+
if (ui.confirm)
|
|
80
|
+
return [['y', 'flatten'], ['any key', 'cancel']];
|
|
81
|
+
if (ui.review)
|
|
82
|
+
return ui.review.stage === 'less-safe' ? [['y', 'apply (less safe)'], ['any key', 'cancel']] : [['y', ui.review.check.ok && ui.review.check.riskDelta === 'looser' ? 'apply… (less safe)' : 'apply'], ['n', 'dismiss'], ['any key', 'close']];
|
|
83
|
+
if (ui.input)
|
|
84
|
+
return [['⏎', 'ok'], ['esc', 'cancel']];
|
|
85
|
+
switch (ui.view) {
|
|
86
|
+
case 'list': return [['tab', 'view'], ['⏎/e', 'edit'], ['n', 'new'], ['d', 'copy'], ['c', 'compare'], ['b', 'backtest'], ['w', 'sweep'], ['x/i', 'export/import'], ['o', 'chat proposal'], ['r', 'runner'], ...common];
|
|
87
|
+
case 'editor': return ui.editor?.typing
|
|
88
|
+
? [['⏎', 'set'], ['esc', 'cancel'], ['ctrl+u', 'clear']]
|
|
89
|
+
: [['tab', 'view'], ['↑↓', 'field'], ['⏎', 'change'], ['s', 'save'], ['u', 'undo'], ['b', 'backtest'], ['w', 'sweep'], ...common];
|
|
90
|
+
case 'compare': return [['tab', 'view'], ['↑↓', 'scroll'], ['b', 'backtest both, one tape'], ...common];
|
|
91
|
+
case 'backtest': return [['tab', 'view'], ['b', 'run again'], ['w', 'sweep (held-out)'], ...common];
|
|
92
|
+
default: return [['tab', 'view'], ['!', 'kill'], ['p', 'pause entries'], ['f', 'flatten (confirm)'], ...common];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ───────────────────────────── strategies ─────────────────────────────
|
|
96
|
+
function renderList(c, ui, a, now) {
|
|
97
|
+
const t = c.theme;
|
|
98
|
+
const bp = breakpoint(c.w);
|
|
99
|
+
const users = ui.entries.filter((e) => e.source === 'user').length;
|
|
100
|
+
// the list takes the rows it needs (min 8); the selected strategy and the chat proposals get the rest
|
|
101
|
+
const listH = Math.min(a.h - 10, Math.max(ui.entries.length + 3, a.h - 16));
|
|
102
|
+
const lowerH = a.h - listH;
|
|
103
|
+
const box = c.box(a.x, a.y, a.w, listH, {
|
|
104
|
+
title: `STRATEGIES · ${ui.entries.length} (${ui.entries.length - users} presets, ${users} yours)`,
|
|
105
|
+
caption: bp === 'compact' ? undefined : 'paper only · your files: ~/.dexterm/strategies · presets are read-only (d makes a copy)', focus: !ui.input,
|
|
106
|
+
});
|
|
107
|
+
const bg = box.bg;
|
|
108
|
+
let y = box.y;
|
|
109
|
+
const active = ui.runner.kind === 'ok' ? ui.runner.snap.strategy : null;
|
|
110
|
+
const wide = bp !== 'compact';
|
|
111
|
+
const cols = wide
|
|
112
|
+
? { mark: 1, id: 4, src: 26, mode: 34, ver: 41, hash: 50, prof: 68, last: Math.max(82, box.w - 44) }
|
|
113
|
+
: { mark: 1, id: 4, src: 24, mode: 31, ver: 37, hash: -1, prof: -1, last: 45 };
|
|
114
|
+
const head = [[cols.id, 'STRATEGY'], [cols.src, 'SOURCE'], [cols.mode, 'MODE'], [cols.ver, 'VERSION'], [cols.hash, 'HASH'], [cols.prof, 'PROFILES'], [cols.last, 'LAST BACKTEST · net · dd · n']];
|
|
115
|
+
for (const [x, h] of head)
|
|
116
|
+
if (x >= 0)
|
|
117
|
+
c.text(box.x + x, y, h, { fg: t.dim, bg });
|
|
118
|
+
y += 1;
|
|
119
|
+
if (!ui.entries.length)
|
|
120
|
+
c.text(box.x + 1, y + 1, 'no strategies found (the shipped presets should always list: reinstall if this stays empty)', { fg: t.warn, bg });
|
|
121
|
+
const rows = box.h - 1;
|
|
122
|
+
const scroll = Math.max(0, ui.selected - (rows - 1));
|
|
123
|
+
for (let i = scroll; i < ui.entries.length && y < box.y + box.h; i++) {
|
|
124
|
+
const e = ui.entries[i];
|
|
125
|
+
const sel = i === ui.selected;
|
|
126
|
+
const rb = sel ? t.selBg : bg;
|
|
127
|
+
c.fill(box.x, y, box.w, 1, ' ', { bg: rb });
|
|
128
|
+
if (sel)
|
|
129
|
+
c.put(box.x, y, '▎', { fg: t.accent, bg: rb, soft: true });
|
|
130
|
+
const running = active && active.id === e.id;
|
|
131
|
+
const marked = ui.marked.includes(e.id);
|
|
132
|
+
c.text(box.x + cols.mark, y, running ? '▶' : marked ? '◆' : ' ', { fg: running ? t.up : t.accent, bg: rb, b: true });
|
|
133
|
+
c.text(box.x + cols.id, y, fit(e.highRisk ? `${e.id} ⚠` : e.id, cols.src - cols.id - 1), { fg: e.ok ? (e.highRisk ? t.warn : sel ? t.borderFocus : t.text) : t.down, bg: rb, b: sel });
|
|
134
|
+
c.text(box.x + cols.src, y, e.source === 'preset' ? 'preset' : e.shadowsPreset ? 'yours*' : 'yours', { fg: e.source === 'preset' ? t.muted : t.accent, bg: rb });
|
|
135
|
+
if (!e.ok) {
|
|
136
|
+
c.text(box.x + cols.mode, y, fit(`✗ invalid: ${e.errors?.[0] ? `${e.errors[0].path || '/'} ${e.errors[0].msg}` : ''}`, box.w - cols.mode - 2), { fg: t.down, bg: rb });
|
|
137
|
+
y += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
c.text(box.x + cols.mode, y, e.mode ?? '—', { fg: e.mode === 'paper' ? t.up : t.warn, bg: rb });
|
|
141
|
+
c.text(box.x + cols.ver, y, `v${e.version}`, { fg: t.muted, bg: rb });
|
|
142
|
+
if (cols.hash >= 0)
|
|
143
|
+
c.text(box.x + cols.hash, y, e.hash ?? '', { fg: t.dim, bg: rb });
|
|
144
|
+
if (cols.prof >= 0)
|
|
145
|
+
c.text(box.x + cols.prof, y, fit((e.profiles ?? []).join(' '), cols.last - cols.prof - 1), { fg: t.muted, bg: rb });
|
|
146
|
+
const lb = e.lastBacktest;
|
|
147
|
+
const job = e.hash ? ui.backtests.get(e.hash) : undefined;
|
|
148
|
+
const last = job?.status === 'running' ? 'running…' : lb ? `${usd(lb.netUsd)} (${pct(lb.netPct)}) · ${lb.maxDrawdownPct.toFixed(1)}% · ${lb.trades}` : 'not backtested';
|
|
149
|
+
c.text(box.x + cols.last, y, fit(last, box.w - cols.last - 2), { fg: lb ? (lb.netUsd >= 0 ? t.up : t.down) : t.dim, bg: rb });
|
|
150
|
+
y += 1;
|
|
151
|
+
}
|
|
152
|
+
// lower pane: the selected strategy + chat proposals
|
|
153
|
+
const ly0 = a.y + listH;
|
|
154
|
+
const sel = ui.entries[ui.selected];
|
|
155
|
+
const props = ui.deps.proposals();
|
|
156
|
+
const half = wide ? Math.floor(a.w * 0.58) : a.w;
|
|
157
|
+
const dbox = c.box(a.x, ly0, half, lowerH, { title: sel ? `${sel.id}${sel.name ? ` · ${sel.name}` : ''}` : 'no strategy', caption: running(ui) ? undefined : 'b backtests on recorded history · r shows the runner' });
|
|
158
|
+
let dy = dbox.y;
|
|
159
|
+
const g = sel ? memo(ui, `get:${entryKey(ui, sel.id)}`, () => ui.deps.store.get(sel.id)) : null;
|
|
160
|
+
if (g?.ok) {
|
|
161
|
+
const s = g.strategy;
|
|
162
|
+
const line = (k, v, fg = t.text) => {
|
|
163
|
+
wrap(v, dbox.w - 13).forEach((l, i) => { if (dy >= dbox.y + dbox.h)
|
|
164
|
+
return; c.text(dbox.x + 1, dy, i ? '' : k.padEnd(10), { fg: t.dim, bg: dbox.bg }); c.text(dbox.x + 11, dy, fit(l, dbox.w - 13), { fg, bg: dbox.bg }); dy += 1; });
|
|
165
|
+
};
|
|
166
|
+
if (s.highRisk)
|
|
167
|
+
line('⚠ HIGH', 'HIGH RISK: full port live (the signer sizes an entry up to the caps you set, up to the whole wallet); one rug can zero it. Experimental, not financial advice.', t.down);
|
|
168
|
+
line('screen', s.entry.screen, t.accent);
|
|
169
|
+
line('age', `${s.entry.ageHours.min}–${s.entry.ageHours.max ?? '∞'} h · ${s.entry.triggers.map((x) => x.kind).join(` ${s.entry.triggerMode} `)} · decide ${s.entry.decide}`);
|
|
170
|
+
line('sizing', `${s.sizing.mode}${s.sizing.pct !== undefined ? ` ${s.sizing.pct}%` : s.sizing.riskPct !== undefined ? ` risk ${s.sizing.riskPct}%` : s.sizing.fixedUsd !== undefined ? ` $${s.sizing.fixedUsd}` : ''} · cap ${s.sizing.maxPctOfBankroll}% / $${s.sizing.maxUsd} · book $${s.bankroll.startUsd}`);
|
|
171
|
+
line('risk', `${s.risk.maxConcurrent} open · ${s.risk.maxExposurePct}% exposure · day halt −${s.risk.dailyLossHaltPct}% · dd ½ at ${s.risk.drawdown.sizeDownAtPct}%, halt ${s.risk.drawdown.haltAtPct}% · ${s.risk.maxEntriesPerDay}/day`);
|
|
172
|
+
for (const p of Object.values(s.exits.profiles))
|
|
173
|
+
line(p.id === s.exits.default ? 'exits ●' : 'exits', ladderSketch(p), t.muted);
|
|
174
|
+
for (const w of g.warnings.slice(0, 2))
|
|
175
|
+
line('warning', `${w.path} ${w.msg}`, t.warn);
|
|
176
|
+
}
|
|
177
|
+
else if (g && !g.ok) {
|
|
178
|
+
for (const e of g.errors.slice(0, dbox.h)) {
|
|
179
|
+
c.text(dbox.x + 1, dy, fit(`✗ ${e.path || '/'} ${e.msg}`, dbox.w - 2), { fg: t.down, bg: dbox.bg });
|
|
180
|
+
dy += 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (wide) {
|
|
184
|
+
const pbox = c.box(a.x + half, ly0, a.w - half, lowerH, { title: `PROPOSALS FROM CHAT · ${props.length}`, caption: 'o reviews · applied only by your keypress' });
|
|
185
|
+
if (!props.length) {
|
|
186
|
+
let py = pbox.y;
|
|
187
|
+
for (const [txt, it] of [['none yet · ask the analyst to change a strategy and its diff card shows up here', true], ['', false], ['a model only ever proposes: it cannot switch a strategy to live, raise its own spend or apply anything', false]]) {
|
|
188
|
+
for (const l of txt ? wrap(txt, pbox.w - 2) : ['']) {
|
|
189
|
+
if (py >= pbox.y + pbox.h)
|
|
190
|
+
break;
|
|
191
|
+
c.text(pbox.x + 1, py, l, { fg: t.dim, bg: pbox.bg, i: it });
|
|
192
|
+
py += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
let py = pbox.y;
|
|
198
|
+
for (const p of props) {
|
|
199
|
+
py = proposalCard(c, p, pbox.x + 1, py, pbox.w - 2, pbox.y + pbox.h);
|
|
200
|
+
if (py >= pbox.y + pbox.h)
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
void now;
|
|
206
|
+
}
|
|
207
|
+
const running = (ui) => ui.runner.kind === 'ok';
|
|
208
|
+
/** task-1528: the running runner's mode from its rendezvous ('live' = mainnet through the live signer). */
|
|
209
|
+
const runMode = (ui) => { const m = ui.runner.kind === 'ok' ? ui.runner.snap.mode : 'paper'; return m === 'live' || m === 'devnet' ? m : 'paper'; };
|
|
210
|
+
/** One chat proposal card (§2a): id, hashes, the diff lines, validation, LOOSER in the warn colour. */
|
|
211
|
+
export function proposalCard(c, p, x, y, w, maxY) {
|
|
212
|
+
const t = c.theme;
|
|
213
|
+
const bg = c.theme.panel;
|
|
214
|
+
const looser = p.riskDelta === 'looser';
|
|
215
|
+
if (y < maxY) {
|
|
216
|
+
c.text(x, y, fit(`${p.id} ${p.beforeHash} → ${p.afterHash}`, w - 10), { fg: t.text, bg, b: true });
|
|
217
|
+
c.textR(x, y, w, looser ? 'LOOSER' : p.riskDelta.toUpperCase(), { fg: looser ? t.warn : p.riskDelta === 'tighter' ? t.up : t.muted, bg, b: true });
|
|
218
|
+
y += 1;
|
|
219
|
+
}
|
|
220
|
+
for (const d of p.diff.slice(0, 4)) {
|
|
221
|
+
if (y >= maxY)
|
|
222
|
+
return y;
|
|
223
|
+
c.text(x + 1, y, fit(d, w - 1), { fg: d.startsWith('+') ? t.up : d.startsWith('-') ? t.down : t.muted, bg });
|
|
224
|
+
y += 1;
|
|
225
|
+
}
|
|
226
|
+
if (y < maxY) {
|
|
227
|
+
c.text(x + 1, y, fit(p.validation.ok ? `✓ valid · ${p.why}` : `✗ ${p.validation.errors[0]?.path ?? ''} ${p.validation.errors[0]?.msg ?? ''}`, w - 1), { fg: p.validation.ok ? t.dim : t.down, bg, i: true });
|
|
228
|
+
y += 1;
|
|
229
|
+
}
|
|
230
|
+
return y + 1;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* task-1507: the review card of one chat proposal (`o`). The full diff, who proposed it and why, the validation, and
|
|
234
|
+
* every less-safe field when it is LOOSER. On a looser edit the first y turns the card into the "less safe" popup and
|
|
235
|
+
* only a second y applies it.
|
|
236
|
+
*/
|
|
237
|
+
function reviewCard(c, ui) {
|
|
238
|
+
const t = c.theme;
|
|
239
|
+
const rv = ui.review;
|
|
240
|
+
const r = rv.rec;
|
|
241
|
+
const chk = rv.check;
|
|
242
|
+
const looser = chk.ok && chk.riskDelta === 'looser';
|
|
243
|
+
const lessSafe = rv.stage === 'less-safe';
|
|
244
|
+
const w = Math.min(96, c.w - 4), h = Math.min(c.h - 4, 24);
|
|
245
|
+
const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
|
|
246
|
+
c.noRain(x, y, w, h);
|
|
247
|
+
const delta = chk.ok ? chk.riskDelta.toUpperCase() : 'CANNOT APPLY';
|
|
248
|
+
const b = c.box(x, y, w, h, { title: `${lessSafe ? '⚠ LESS SAFE · ' : ''}CHAT PROPOSAL · ${r.kind === 'new' ? `new ${r.id} from ${r.from}` : r.id} · ${delta} · PAPER`, focus: true, bg: t.panelAlt, border: looser || !chk.ok ? t.warn : t.accent });
|
|
249
|
+
const bg = b.bg;
|
|
250
|
+
let yy = b.y;
|
|
251
|
+
const line = (s, st) => { if (yy < b.y + b.h) {
|
|
252
|
+
c.text(b.x + 1, yy, fit(s, b.w - 2), { ...st, bg });
|
|
253
|
+
yy += 1;
|
|
254
|
+
} };
|
|
255
|
+
line(`proposed by ${r.actor} · ${r.proposalId} · ${r.beforeHash} → ${r.afterHash} · from v${r.baseVersion}`, { fg: t.muted });
|
|
256
|
+
for (const l of wrap(`why: ${r.why}`, b.w - 2).slice(0, 2))
|
|
257
|
+
line(l, { fg: t.text, i: true });
|
|
258
|
+
if (!chk.ok) {
|
|
259
|
+
yy += 1;
|
|
260
|
+
for (const e of chk.errors.slice(0, 6))
|
|
261
|
+
line(`✗ ${e.path || '/'} ${e.msg}`, { fg: t.down });
|
|
262
|
+
line('n dismisses it · any other key closes', { fg: t.warn, b: true });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const less = chk.reasons.filter((x) => x.dir === 'looser');
|
|
266
|
+
if (lessSafe) {
|
|
267
|
+
yy += 1;
|
|
268
|
+
line('THIS IS LESS SAFE. These risk numbers get looser:', { fg: t.warn, b: true });
|
|
269
|
+
for (const x of less.slice(0, Math.max(1, b.h - 7)))
|
|
270
|
+
line(` ${x.path} ${x.from} → ${x.to}${x.note ? ` (${x.note})` : ''}`, { fg: t.warn });
|
|
271
|
+
yy = Math.max(yy, b.y + b.h - 2);
|
|
272
|
+
line('y applies it anyway (a new version, journaled) · any other key cancels', { fg: t.warn, b: true });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
line(`✓ valid${chk.validation.warnings.length ? ` · ${chk.validation.warnings.length} warning(s): ${chk.validation.warnings[0].path} ${chk.validation.warnings[0].msg}` : ''}`, { fg: t.up });
|
|
276
|
+
if (looser)
|
|
277
|
+
line(`LOOSER: ${less.map((x) => x.path.split('/').slice(-2).join('/')).join(', ')}`, { fg: t.warn, b: true });
|
|
278
|
+
const room = b.y + b.h - 1 - yy; // the last row is the key line
|
|
279
|
+
const diff = chk.diff.length > room ? [...chk.diff.slice(0, Math.max(0, room - 1)), `… ${chk.diff.length - room + 1} more lines`] : chk.diff;
|
|
280
|
+
for (const d of diff)
|
|
281
|
+
line(d, { fg: d.startsWith('+') ? t.up : d.startsWith('-') ? t.down : t.muted });
|
|
282
|
+
c.text(b.x + 1, b.y + b.h - 1, fit(looser ? 'y opens the less-safe popup (a second y applies) · n dismisses · any other key closes' : 'y applies it (a new version, journaled) · n dismisses · any other key closes', b.w - 2), { fg: looser ? t.warn : t.accent, bg, b: true });
|
|
283
|
+
}
|
|
284
|
+
/** A one-line sketch of an exit profile: stop · cost-back · TP steps · trails · time/hold limits. */
|
|
285
|
+
export function ladderSketch(p) {
|
|
286
|
+
const parts = [];
|
|
287
|
+
parts.push(p.hardStop.enabled && p.hardStop.atPct !== null ? `stop ${p.hardStop.atPct}% ┤` : 'no stop ┤');
|
|
288
|
+
parts.push('entry');
|
|
289
|
+
const steps = p.tpLadder.map((s) => [s.atPct, `+${s.atPct}% ${frac(s.sellFrac)}`]);
|
|
290
|
+
if (p.costRecovery)
|
|
291
|
+
steps.push([p.costRecovery.atPct, `+${p.costRecovery.atPct}% cost back (≤${Math.round(p.costRecovery.maxSellFrac * 100)}%)`]);
|
|
292
|
+
steps.sort((x, y) => x[0] - y[0]);
|
|
293
|
+
for (const [, s] of steps)
|
|
294
|
+
parts.push(s);
|
|
295
|
+
const tail = [];
|
|
296
|
+
if (p.trailingStop)
|
|
297
|
+
tail.push(`trail ${p.trailingStop.trailPct}% after +${p.trailingStop.armAtPct}%`);
|
|
298
|
+
if (p.bagTrail)
|
|
299
|
+
tail.push(`bag trail ${p.bagTrail.trailPct}%${p.bagTrail.armAtPct ? ` after +${p.bagTrail.armAtPct}%` : ''}`);
|
|
300
|
+
if (p.timeStop)
|
|
301
|
+
tail.push(`${p.timeStop.minutes}m ${p.timeStop.action}`);
|
|
302
|
+
if (p.maxHoldMinutes)
|
|
303
|
+
tail.push(`hold ≤ ${p.maxHoldMinutes >= 1440 ? `${+(p.maxHoldMinutes / 1440).toFixed(1)}d` : p.maxHoldMinutes >= 60 ? `${+(p.maxHoldMinutes / 60).toFixed(1)}h` : `${p.maxHoldMinutes}m`}`);
|
|
304
|
+
return `${p.id}: ${parts.join(' ── ')}${tail.length ? ` · ${tail.join(' · ')}` : ''}`;
|
|
305
|
+
}
|
|
306
|
+
const frac = (f) => ({ 0.5: '½', 0.25: '¼', 0.75: '¾' }[f] ?? (Math.abs(f - 1 / 3) < 0.01 ? '⅓' : `${Math.round(f * 100)}%`));
|
|
307
|
+
// ───────────────────────────── editor ─────────────────────────────
|
|
308
|
+
function renderEditor(c, ui, a) {
|
|
309
|
+
const t = c.theme;
|
|
310
|
+
const ed = ui.editor;
|
|
311
|
+
if (!ed) {
|
|
312
|
+
c.text(a.x + 1, a.y + 1, 'no strategy open · e on the list opens one', { fg: t.dim, bg: t.bg });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const wide = breakpoint(c.w) !== 'compact';
|
|
316
|
+
const formW = wide ? Math.floor(a.w * 0.6) : a.w;
|
|
317
|
+
const inputH = ed.typing ? 4 : 0;
|
|
318
|
+
const check = ed.typing?.check ?? ed.check;
|
|
319
|
+
const unsaved = ed.edits.length;
|
|
320
|
+
const status = ed.typing ? (ed.typing.parseError ? '✗ not a value' : check.ok ? '✓ valid' : `✗ ${check.errors.length} problem${check.errors.length === 1 ? '' : 's'}`) : check.ok ? '✓ valid' : `✗ ${check.errors.length} problem${check.errors.length === 1 ? '' : 's'}`;
|
|
321
|
+
const box = c.box(a.x, a.y, formW, a.h - inputH, {
|
|
322
|
+
title: `${ed.id} · v${ed.baseVersion}${unsaved ? ` + ${unsaved} unsaved` : ''} · ${ed.draft.mode.toUpperCase()} · ${status}`,
|
|
323
|
+
caption: ed.source === 'preset' ? 'shipped preset: read-only (d on the list makes your copy)' : 's saves a new version (only when valid) · u undoes', focus: !ed.typing,
|
|
324
|
+
});
|
|
325
|
+
const bg = box.bg;
|
|
326
|
+
const { at } = issuesByField(ed.fields, ed.check.ok ? [] : ed.check.errors);
|
|
327
|
+
const typingIssues = ed.typing?.check && !ed.typing.check.ok ? ed.typing.check.errors : [];
|
|
328
|
+
const rows = [];
|
|
329
|
+
let last = '';
|
|
330
|
+
ed.fields.forEach((f, i) => { if (f.section !== last) {
|
|
331
|
+
rows.push({ kind: 'section', s: f.section });
|
|
332
|
+
last = f.section;
|
|
333
|
+
} rows.push({ kind: 'field', i }); });
|
|
334
|
+
const selRow = rows.findIndex((r) => r.kind === 'field' && r.i === ed.selected);
|
|
335
|
+
const view = box.h;
|
|
336
|
+
const scroll = Math.max(0, Math.min(rows.length - view, selRow - Math.floor(view / 2)));
|
|
337
|
+
const labelW = Math.min(36, Math.max(...ed.fields.map((f) => f.path.length - f.section.length)) + 2, Math.floor(box.w * 0.45));
|
|
338
|
+
let y = box.y;
|
|
339
|
+
for (let r = scroll; r < rows.length && y < box.y + box.h; r++, y++) {
|
|
340
|
+
const row = rows[r];
|
|
341
|
+
if (row.kind === 'section') {
|
|
342
|
+
c.text(box.x + 1, y, row.s.toUpperCase(), { fg: t.accent, bg, b: true });
|
|
343
|
+
c.hline(box.x + 2 + row.s.length, y, box.w - row.s.length - 4, { fg: t.border, bg, soft: true });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const f = ed.fields[row.i];
|
|
347
|
+
const sel = row.i === ed.selected;
|
|
348
|
+
const rb = sel ? t.selBg : bg;
|
|
349
|
+
c.fill(box.x, y, box.w, 1, ' ', { bg: rb });
|
|
350
|
+
if (sel)
|
|
351
|
+
c.put(box.x, y, '▎', { fg: t.accent, bg: rb, soft: true });
|
|
352
|
+
const changed = ed.edits.some((x) => x.op.path === f.path);
|
|
353
|
+
c.text(box.x + 2, y, fit(f.path.slice(f.section.length + 2) || f.path, labelW - 2), { fg: READ_ONLY_PATHS.has(f.path) ? t.dim : changed ? t.warn : t.muted, bg: rb });
|
|
354
|
+
const vx = box.x + labelW + 1;
|
|
355
|
+
const vw = box.w - labelW - 3;
|
|
356
|
+
const v = getAt(ed.draft, f.path);
|
|
357
|
+
const errs = at.get(f.path) ?? [];
|
|
358
|
+
const text = f.kind === 'string' ? String(v) : JSON.stringify(v);
|
|
359
|
+
// the value gets what it needs; an error takes the rest of the row
|
|
360
|
+
const valW = errs.length ? Math.min(text.length + 1, Math.floor(vw * 0.5)) : vw;
|
|
361
|
+
if (f.path === '/entry/screen')
|
|
362
|
+
screenValue(c, String(v ?? ''), vx, y, valW, rb);
|
|
363
|
+
else
|
|
364
|
+
c.text(vx, y, fit(text, valW), { fg: changed ? t.warn : t.text, bg: rb, b: changed });
|
|
365
|
+
if (errs.length) {
|
|
366
|
+
const mx = vx + valW + 1;
|
|
367
|
+
c.text(mx, y, fit(`✗ ${errs[0].msg}`, box.x + box.w - mx - 1), { fg: t.down, bg: rb });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (ed.typing) {
|
|
371
|
+
const iy = a.y + a.h - inputH;
|
|
372
|
+
const ib = c.box(a.x, iy, formW, inputH, { title: `set ${ed.typing.path}`, caption: '⏎ set · esc cancel · re-validated on every key', focus: true });
|
|
373
|
+
const shownW = ib.w - 4;
|
|
374
|
+
const txt = ed.typing.text;
|
|
375
|
+
const shown = txt.length > shownW ? '…' + txt.slice(txt.length - shownW + 1) : txt;
|
|
376
|
+
if (ed.typing.path === '/entry/screen')
|
|
377
|
+
screenValue(c, shown, ib.x + 1, ib.y, shownW, ib.bg);
|
|
378
|
+
else
|
|
379
|
+
c.text(ib.x + 1, ib.y, shown, { fg: t.text, bg: ib.bg });
|
|
380
|
+
c.put(ib.x + 1 + Math.min(shown.length, shownW), ib.y, '█', { fg: t.accent, bg: ib.bg, soft: true });
|
|
381
|
+
// the live result under the value: for the screen filter a caret at the parser's error column
|
|
382
|
+
const ty = ed.typing;
|
|
383
|
+
const own = ty.check && !ty.check.ok ? ty.check.errors.find((e) => e.path === ty.path || e.path.startsWith(`${ty.path}/`)) : undefined;
|
|
384
|
+
const sh = ty.path === '/entry/screen' ? screenHighlight(txt) : null;
|
|
385
|
+
const res = ty.parseError ? `✗ ${ty.parseError}` : own ? `✗ ${own.msg}` : ty.check && !ty.check.ok ? `✓ this field · ✗ ${ty.check.errors.length} elsewhere` : '✓ valid';
|
|
386
|
+
const shift = txt.length - shown.length;
|
|
387
|
+
if (sh?.at && sh.at - 1 - shift >= 0 && sh.at - 1 - shift < shownW) {
|
|
388
|
+
const cx = ib.x + 1 + sh.at - 1 - shift;
|
|
389
|
+
c.put(cx, ib.y + 1, '^', { fg: t.down, bg: ib.bg, b: true });
|
|
390
|
+
c.text(cx + 2, ib.y + 1, fit(res, ib.x + ib.w - cx - 4), { fg: t.down, bg: ib.bg });
|
|
391
|
+
}
|
|
392
|
+
else
|
|
393
|
+
c.text(ib.x + 1, ib.y + 1, fit(res, ib.w - 2), { fg: res.startsWith('✓') ? t.up : t.down, bg: ib.bg });
|
|
394
|
+
}
|
|
395
|
+
if (!wide)
|
|
396
|
+
return;
|
|
397
|
+
// right column: validation of the draft (or of the value being typed) + the exit ladders
|
|
398
|
+
const rx = a.x + formW, rw = a.w - formW;
|
|
399
|
+
const vh = Math.max(6, Math.floor(a.h * 0.45));
|
|
400
|
+
const vbox = c.box(rx, a.y, rw, vh, { title: ed.typing ? 'VALIDATION · with the value you are typing' : 'VALIDATION', caption: 'dexterm-strategy/1 + cross-field rules' });
|
|
401
|
+
let vy = vbox.y;
|
|
402
|
+
const vline = (s, fg) => { wrap(s, vbox.w - 4).forEach((l, i) => { if (vy < vbox.y + vbox.h) {
|
|
403
|
+
c.text(vbox.x + 1 + (i ? 2 : 0), vy, fit(l, vbox.w - 2 - (i ? 2 : 0)), { fg, bg: vbox.bg });
|
|
404
|
+
vy += 1;
|
|
405
|
+
} }); };
|
|
406
|
+
if (ed.typing?.parseError)
|
|
407
|
+
vline(`✗ ${ed.typing.parseError}`, t.down);
|
|
408
|
+
const shownIssues = ed.typing ? typingIssues : ed.check.ok ? [] : ed.check.errors;
|
|
409
|
+
if (!ed.typing?.parseError && !shownIssues.length)
|
|
410
|
+
vline('✓ valid: every field and the cross-field rules pass', t.up);
|
|
411
|
+
for (const i of shownIssues)
|
|
412
|
+
vline(`✗ ${i.path || '/'} ${i.msg}`, t.down);
|
|
413
|
+
for (const w of (ed.typing?.check ?? ed.check).warnings)
|
|
414
|
+
vline(`! ${w.path} ${w.msg}`, t.warn);
|
|
415
|
+
const lbox = c.box(rx, a.y + vh, rw, a.h - vh, { title: 'EXIT PROFILES', caption: 'stop ┤ entry ── take-profits ── trails · limits' });
|
|
416
|
+
let ly = lbox.y;
|
|
417
|
+
const chk = ed.check;
|
|
418
|
+
const profiles = chk.ok ? Object.values(chk.strategy.exits.profiles) : [];
|
|
419
|
+
if (!profiles.length)
|
|
420
|
+
c.text(lbox.x + 1, ly, 'fix the problems to see the ladders', { fg: t.dim, bg: lbox.bg, i: true });
|
|
421
|
+
for (const p of profiles) {
|
|
422
|
+
if (ly >= lbox.y + lbox.h - 1)
|
|
423
|
+
break;
|
|
424
|
+
c.text(lbox.x + 1, ly, fit(`${p.id === ed.draft.exits.default ? '● ' : ' '}${p.name}`, lbox.w - 2), { fg: t.accent, bg: lbox.bg, b: true });
|
|
425
|
+
ly += 1;
|
|
426
|
+
for (const l of wrapSketch(ladderSketch(p).replace(/^[^:]+: /, ''), lbox.w - 4).slice(0, 2)) {
|
|
427
|
+
c.text(lbox.x + 3, ly, l, { fg: t.muted, bg: lbox.bg });
|
|
428
|
+
ly += 1;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function wrapSketch(s, w) {
|
|
433
|
+
const out = [];
|
|
434
|
+
let cur = '';
|
|
435
|
+
for (const part of s.split(' · ')) {
|
|
436
|
+
const next = cur ? `${cur} · ${part}` : part;
|
|
437
|
+
if (next.length > w && cur) {
|
|
438
|
+
out.push(cur);
|
|
439
|
+
cur = part;
|
|
440
|
+
}
|
|
441
|
+
else
|
|
442
|
+
cur = next;
|
|
443
|
+
}
|
|
444
|
+
if (cur)
|
|
445
|
+
out.push(fit(cur, w));
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
/** The screen filter in the screener's own tokens: fields / ops / tags coloured, the parser's error column lit. */
|
|
449
|
+
function screenValue(c, src, x, y, w, bg) {
|
|
450
|
+
const t = c.theme;
|
|
451
|
+
const shown = fit(src, w);
|
|
452
|
+
c.text(x, y, shown, { fg: t.text, bg });
|
|
453
|
+
for (const tok of tokenize(shown)) {
|
|
454
|
+
const fg = tok.kind === 'op' ? t.warn : tok.kind === 'not' ? t.down : tok.kind === 'colon' ? t.dim : /^[0-9.]/.test(tok.text) ? t.up : t.accent;
|
|
455
|
+
c.text(x + tok.at - 1, y, tok.text, { fg, bg });
|
|
456
|
+
}
|
|
457
|
+
const h = screenHighlight(src);
|
|
458
|
+
if (h.at !== null && h.at - 1 < shown.length)
|
|
459
|
+
c.put(x + Math.max(0, h.at - 1), y, shown[Math.max(0, h.at - 1)] ?? ' ', { fg: t.bg, bg: t.down, b: true });
|
|
460
|
+
}
|
|
461
|
+
// ───────────────────────────── compare ─────────────────────────────
|
|
462
|
+
function renderCompare(c, ui, a) {
|
|
463
|
+
const t = c.theme;
|
|
464
|
+
const cmp = ui.compare;
|
|
465
|
+
if (!cmp) {
|
|
466
|
+
c.text(a.x + 1, a.y + 1, 'c on two strategies in the list compares them', { fg: t.dim, bg: t.bg });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const store = ui.deps.store;
|
|
470
|
+
const { rows, lastA, lastB } = memo(ui, `cmp:${entryKey(ui, cmp.a)}:${entryKey(ui, cmp.b)}`, () => {
|
|
471
|
+
const ga = store.get(cmp.a), gb = store.get(cmp.b);
|
|
472
|
+
const bts = store.backtests();
|
|
473
|
+
return {
|
|
474
|
+
rows: compareRows(store.raw(cmp.a), store.raw(cmp.b)),
|
|
475
|
+
lastA: ga?.ok ? bts[ga.strategy.hash] : undefined, lastB: gb?.ok ? bts[gb.strategy.hash] : undefined,
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
const changed = rows.filter((r) => r.changed).length;
|
|
479
|
+
const btH = 6;
|
|
480
|
+
const bt = c.box(a.x, a.y, a.w, btH, { title: 'BACKTESTS · same tape', caption: 'b runs both on one window of recorded history' });
|
|
481
|
+
const half = Math.floor((bt.w - 2) / 2);
|
|
482
|
+
const job = ui.compareRun;
|
|
483
|
+
const cell = (i, id, last, x) => {
|
|
484
|
+
let y = bt.y;
|
|
485
|
+
c.text(x, y, fit(id, half - 1), { fg: t.accent, bg: bt.bg, b: true });
|
|
486
|
+
y += 1;
|
|
487
|
+
const out = job?.status === 'done' ? job.result[i] : null;
|
|
488
|
+
if (out) {
|
|
489
|
+
for (const l of reportLines(out.report).slice(0, 3)) {
|
|
490
|
+
c.text(x, y, fit(l, half - 1), { fg: out.report.netUsd >= 0 ? t.up : t.down, bg: bt.bg });
|
|
491
|
+
y += 1;
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const line = job?.status === 'running' ? 'running…' : job?.status === 'error' ? `✗ ${job.error}` : last ? `last: ${usd(last.netUsd)} · dd ${last.maxDrawdownPct.toFixed(1)}% · ${last.trades} trades · ${last.tape}` : 'not backtested yet';
|
|
496
|
+
c.text(x, y, fit(line, half - 1), { fg: job?.status === 'error' ? t.down : t.muted, bg: bt.bg });
|
|
497
|
+
};
|
|
498
|
+
cell(0, cmp.a, lastA, bt.x + 1);
|
|
499
|
+
cell(1, cmp.b, lastB, bt.x + 1 + half);
|
|
500
|
+
if (job?.status === 'done')
|
|
501
|
+
c.text(bt.x + 1, bt.y + bt.h - 1, fit(`tape: ${job.result[0].report.tape.mints} coins · ${job.result[0].report.tape.from ?? '?'} → ${job.result[0].report.tape.to ?? '?'} (both)`, bt.w - 2), { fg: t.dim, bg: bt.bg });
|
|
502
|
+
const box = c.box(a.x, a.y + btH, a.w, a.h - btH, { title: `FIELDS · ${changed} differ of ${rows.length}`, caption: 'changed fields lit · builtin profiles compare as references', focus: true });
|
|
503
|
+
const pw = Math.min(46, Math.floor(box.w * 0.34));
|
|
504
|
+
const vw = Math.floor((box.w - pw - 3) / 2);
|
|
505
|
+
c.text(box.x + 1, box.y, 'FIELD', { fg: t.dim, bg: box.bg });
|
|
506
|
+
c.text(box.x + 1 + pw, box.y, fit(cmp.a, vw), { fg: t.dim, bg: box.bg });
|
|
507
|
+
c.text(box.x + 2 + pw + vw, box.y, fit(cmp.b, vw), { fg: t.dim, bg: box.bg });
|
|
508
|
+
// changed rows first, then the rest, so the differences are on the first screen
|
|
509
|
+
const ordered = [...rows.filter((r) => r.changed), ...rows.filter((r) => !r.changed)];
|
|
510
|
+
cmp.scroll = Math.max(0, Math.min(cmp.scroll, Math.max(0, ordered.length - (box.h - 1))));
|
|
511
|
+
let y = box.y + 1;
|
|
512
|
+
for (let i = cmp.scroll; i < ordered.length && y < box.y + box.h; i++, y++) {
|
|
513
|
+
const r = ordered[i];
|
|
514
|
+
const rbg = r.changed ? t.selBg : box.bg;
|
|
515
|
+
c.fill(box.x, y, box.w, 1, ' ', { bg: rbg });
|
|
516
|
+
c.text(box.x + 1, y, fit(r.path, pw - 1), { fg: r.changed ? t.warn : t.dim, bg: rbg });
|
|
517
|
+
c.text(box.x + 1 + pw, y, fit(r.a, vw - 1), { fg: r.changed ? t.text : t.muted, bg: rbg, b: r.changed });
|
|
518
|
+
c.text(box.x + 2 + pw + vw, y, fit(r.b, vw - 1), { fg: r.changed ? t.text : t.muted, bg: rbg, b: r.changed });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ───────────────────────────── backtest ─────────────────────────────
|
|
522
|
+
export function reportLines(r) {
|
|
523
|
+
const wr = r.winRate ? `${r.winRate.value.toFixed(1)}% (95% ${r.winRate.lo.toFixed(1)}–${r.winRate.hi.toFixed(1)}%)` : '—';
|
|
524
|
+
return [
|
|
525
|
+
`${r.trades} trades · win rate ${wr} · net ${usd(r.netUsd)} (${pct(r.netPct)})`,
|
|
526
|
+
`max drawdown ${r.maxDrawdownPct.toFixed(1)}% · profit factor ${r.profitFactor === null ? '—' : r.profitFactor.toFixed(2)} · best ${pct(r.bestPct)} · worst ${pct(r.worstPct)}`,
|
|
527
|
+
`stuck ${r.stuck} · open at end ${r.openAtEnd} · candidates ${r.candidates} · rugs ${r.tail.rugs} · cost back ${r.tail.costRecovered} · net without the best trade ${usd(r.tail.netWithoutBestUsd)}`,
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
function renderBacktest(c, ui, a, now) {
|
|
531
|
+
const t = c.theme;
|
|
532
|
+
const subj = backtestSubject(ui);
|
|
533
|
+
const job = subj?.hash ? ui.backtests.get(subj.hash) : undefined;
|
|
534
|
+
const sw = ui.sweep && subj && ui.sweep.id === subj.id ? ui.sweep : null;
|
|
535
|
+
const sweepH = sw ? Math.min(11, Math.max(7, Math.floor(a.h * 0.32))) : 0;
|
|
536
|
+
const box = c.box(a.x, a.y, a.w, a.h - sweepH, {
|
|
537
|
+
title: `BACKTEST · ${subj?.id ?? 'no strategy'} · PAPER · recorded history, modelled fills`,
|
|
538
|
+
caption: job?.status === 'done' && job.result ? `${job.result.report.tape.mints} coins · ${job.result.report.tape.from ?? '?'} → ${job.result.report.tape.to ?? '?'}` : 'b runs it · w sweeps with a held-out check',
|
|
539
|
+
focus: true,
|
|
540
|
+
});
|
|
541
|
+
const bg = box.bg;
|
|
542
|
+
let y = box.y;
|
|
543
|
+
const line = (s, fg = t.text, b = false) => { if (y < box.y + box.h) {
|
|
544
|
+
c.text(box.x + 1, y, fit(s, box.w - 2), { fg, bg, b });
|
|
545
|
+
y += 1;
|
|
546
|
+
} };
|
|
547
|
+
if (!job)
|
|
548
|
+
line(subj ? `${subj.id} has no backtest in this session yet · b runs it on the last 14 days of recorded history` : 'select a strategy on the list', t.dim);
|
|
549
|
+
else if (job.status === 'running')
|
|
550
|
+
line(`running… ${fmtAgeSec(Math.max(0, Math.round((now - job.startedAt) / 1000)))} (off the screen loop; the app stays live)`, t.muted);
|
|
551
|
+
else if (job.status === 'error')
|
|
552
|
+
line(`✗ ${job.error}`, t.down);
|
|
553
|
+
const r = job?.status === 'done' ? job.result?.report : undefined;
|
|
554
|
+
if (r && job?.result) {
|
|
555
|
+
const ls = reportLines(r);
|
|
556
|
+
line(ls[0], r.netUsd >= 0 ? t.up : t.down, true);
|
|
557
|
+
line(ls[1]);
|
|
558
|
+
line(ls[2], t.muted);
|
|
559
|
+
if (r.tail.perEntry)
|
|
560
|
+
line(`per entry ${pct(r.tail.perEntry.meanPct)} (bootstrap 95% ${pct(r.tail.perEntry.lo)} … ${pct(r.tail.perEntry.hi)})${r.tail.multiples ? ` · exit multiples P50 ${r.tail.multiples.p50.toFixed(2)}× · P90 ${r.tail.multiples.p90.toFixed(2)}× · max ${r.tail.multiples.max.toFixed(2)}×` : ''}`, t.muted);
|
|
561
|
+
if (r.trades < 20)
|
|
562
|
+
line('n < 20: read the counts, not the rates', t.warn, true);
|
|
563
|
+
// equity sparkline (marked equity: stuck bags count as 0)
|
|
564
|
+
if (y < box.y + box.h) {
|
|
565
|
+
c.text(box.x + 1, y, 'equity', { fg: t.dim, bg });
|
|
566
|
+
sparkline(c, box.x + 9, y, Math.min(box.w - 11, 90), job.result.curve, { bg, color: r.netUsd >= 0 ? t.up : t.down });
|
|
567
|
+
y += 2;
|
|
568
|
+
}
|
|
569
|
+
// attribution tables, side by side
|
|
570
|
+
const tables = [['BY EXIT RULE', r.byExit], ['BY PROFILE', r.byProfile], ['BY TRIGGER', r.byTrigger]];
|
|
571
|
+
const tw = Math.floor((box.w - 2) / 3);
|
|
572
|
+
const ty0 = y;
|
|
573
|
+
let maxY = y;
|
|
574
|
+
tables.forEach(([title, rows], i) => {
|
|
575
|
+
let yy = ty0;
|
|
576
|
+
const x = box.x + 1 + i * tw;
|
|
577
|
+
if (yy < box.y + box.h)
|
|
578
|
+
c.text(x, yy, title, { fg: t.accent, bg, b: true });
|
|
579
|
+
yy += 1;
|
|
580
|
+
for (const [k, b] of Object.entries(rows).sort((p, q) => p[1].pnlUsd - q[1].pnlUsd).slice(0, 6)) {
|
|
581
|
+
if (yy >= box.y + box.h)
|
|
582
|
+
break;
|
|
583
|
+
c.text(x, yy, fit(k, tw - 18), { fg: t.muted, bg });
|
|
584
|
+
c.textR(x, yy, tw - 2, `${String(b.n).padStart(3)} ${usd(b.pnlUsd).padStart(10)}`, { fg: b.pnlUsd >= 0 ? t.up : t.down, bg });
|
|
585
|
+
yy += 1;
|
|
586
|
+
}
|
|
587
|
+
maxY = Math.max(maxY, yy);
|
|
588
|
+
});
|
|
589
|
+
y = maxY + 1;
|
|
590
|
+
const clamps = Object.entries(r.clamps).map(([k, n]) => `${k} ${n}`).join(' · ') || 'none';
|
|
591
|
+
const refusals = Object.entries(r.refusals).sort((p, q) => q[1] - p[1]).slice(0, 6).map(([k, n]) => `${k} ${n}`).join(' · ') || 'none';
|
|
592
|
+
line(`clamps fired: ${clamps}`, t.muted);
|
|
593
|
+
line(`refusals: ${refusals}`, t.muted);
|
|
594
|
+
y += 1;
|
|
595
|
+
// the data-limits box, always printed
|
|
596
|
+
if (y < box.y + box.h) {
|
|
597
|
+
const lh = Math.min(r.limits.length + 2, box.y + box.h - y);
|
|
598
|
+
if (lh >= 3) {
|
|
599
|
+
const lb = c.box(box.x + 1, y, box.w - 2, lh, { title: 'LIMITS (read before trusting any number)', bg: t.panelAlt, border: t.warn });
|
|
600
|
+
r.limits.slice(0, lb.h).forEach((l, i) => c.text(lb.x + 1, lb.y + i, fit(`· ${l}`, lb.w - 2), { fg: t.muted, bg: lb.bg }));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else if (y < box.y + box.h - 3) {
|
|
605
|
+
y += 1;
|
|
606
|
+
line('What a backtest here is: the same engine as the runner (entry rules, clamp, exits) replayed over the recorder\'s history,', t.dim);
|
|
607
|
+
line('with mechanical entries (no agent), modelled fills, stuck bags valued at 0, and the limits box printed with every report.', t.dim);
|
|
608
|
+
}
|
|
609
|
+
if (sw) {
|
|
610
|
+
const sb = c.box(a.x, a.y + a.h - sweepH, a.w, sweepH, { title: `SWEEP · ${Object.keys(sw.grid).join(' × ')} · held-out = newest 30% of coins`, caption: 'tuned on train only; the held-out coins are touched twice' });
|
|
611
|
+
let sy = sb.y;
|
|
612
|
+
const sl = (s, fg = t.text, b = false) => { if (sy < sb.y + sb.h) {
|
|
613
|
+
c.text(sb.x + 1, sy, fit(s, sb.w - 2), { fg, bg: sb.bg, b });
|
|
614
|
+
sy += 1;
|
|
615
|
+
} };
|
|
616
|
+
if (sw.status === 'running')
|
|
617
|
+
sl(`running… ${fmtAgeSec(Math.max(0, Math.round((now - sw.startedAt) / 1000)))}`, t.muted);
|
|
618
|
+
else if (sw.status === 'error')
|
|
619
|
+
sl(`✗ ${sw.error}`, t.down);
|
|
620
|
+
else if (sw.result) {
|
|
621
|
+
const s = sw.result;
|
|
622
|
+
const gain = /^held-out gain/.test(s.verdict);
|
|
623
|
+
sl(`VERDICT: ${s.verdict}`, gain ? t.up : t.warn, true);
|
|
624
|
+
sl(`split: train ${s.split.trainMints} coins · held-out ${s.split.testMints} coins (newest, from ${s.split.cutoff}) · ${s.combos} combinations run · ${s.invalid.length} refused as invalid`, t.muted);
|
|
625
|
+
const pw = Math.max(9, ...[s.baseline, s.best, ...s.top].map((x) => paramText(x).length));
|
|
626
|
+
sl(`unchanged ${sweepRow(s.baseline, pw)}`);
|
|
627
|
+
sl(`train best ${sweepRow(s.best, pw)}`);
|
|
628
|
+
sl('top on train:', t.dim);
|
|
629
|
+
for (const x of s.top.slice(0, Math.max(0, sb.h - 5)))
|
|
630
|
+
sl(` ${sweepRow(x, pw)}`, t.dim);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const paramText = (x) => Object.entries(x.params).map(([k, v]) => `${k.replace(/^profile\./, '')}=${v}`).join(' ') || '(as is)';
|
|
635
|
+
function sweepRow(x, pw) {
|
|
636
|
+
return `${paramText(x).padEnd(pw)} train ${String(x.train.trades).padStart(3)} ${usd(x.train.netUsd).padStart(10)} dd ${x.train.maxDrawdownPct.toFixed(1)}%${x.test ? ` · held-out ${String(x.test.trades).padStart(3)} ${usd(x.test.netUsd).padStart(10)} dd ${x.test.maxDrawdownPct.toFixed(1)}%` : ''}`;
|
|
637
|
+
}
|
|
638
|
+
// ───────────────────────────── runner ─────────────────────────────
|
|
639
|
+
function renderRunner(c, ui, a, now) {
|
|
640
|
+
const t = c.theme;
|
|
641
|
+
const rr = ui.runner;
|
|
642
|
+
const sh = 5;
|
|
643
|
+
const mode = runMode(ui);
|
|
644
|
+
const sbox = c.box(a.x, a.y, a.w, sh, { title: `RUNNER · ${mode === 'live' ? 'LIVE · MAINNET' : mode === 'devnet' ? 'DEVNET (live signer rehearsal)' : 'PAPER'}`, caption: `status file: ${rr.path || '~/.dexterm/autotrade/runner.json'}`, focus: true });
|
|
645
|
+
let y = sbox.y;
|
|
646
|
+
const sl = (s, fg = t.text, b = false) => { if (y < sbox.y + sbox.h) {
|
|
647
|
+
c.text(sbox.x + 1, y, fit(s, sbox.w - 2), { fg, bg: sbox.bg, b });
|
|
648
|
+
y += 1;
|
|
649
|
+
} };
|
|
650
|
+
if (rr.kind === 'none') {
|
|
651
|
+
sl('no autotrade runner is running', t.muted, true);
|
|
652
|
+
sl('the runner (headless, one per machine) writes its status here every heartbeat; this view picks it up within 2 s', t.dim);
|
|
653
|
+
sl('`!` kill · `p` pause entries · `f` flatten act on a running runner only; nothing on this screen reaches a wallet or the signer', t.dim);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (rr.kind === 'bad') {
|
|
657
|
+
sl(`✗ status file unreadable: ${rr.error}`, t.down, true);
|
|
658
|
+
sl('the view shows nothing it cannot verify', t.dim);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const s = rr.snap;
|
|
662
|
+
const stale = heartbeatStale(rr, now);
|
|
663
|
+
const age = fmtAgeSec(Math.max(0, Math.round((now - s.heartbeatAt) / 1000)));
|
|
664
|
+
const stateFg = stale ? t.down : s.state === 'running' ? t.up : s.state === 'paused' ? t.warn : t.down;
|
|
665
|
+
sl(`${stale ? 'STALE' : s.state.toUpperCase()} · ${s.strategy.id} v${s.strategy.version} (${s.strategy.hash}) · pid ${s.pid} · heartbeat ${age} ago${stale ? ' (no heartbeat for over 30 s: treat it as down)' : ''}`, stateFg, true);
|
|
666
|
+
sl(`book $${s.book.equityUsd.toFixed(2)} of $${s.book.startUsd.toFixed(2)} · cash $${s.book.cashUsd.toFixed(2)} · realized today ${usd(s.book.realizedTodayUsd)} · model spend $${s.spend.todayUsd.toFixed(2)} of $${s.spend.dayCapUsd.toFixed(2)} today`);
|
|
667
|
+
sl(s.haltReason ? `halted: ${s.haltReason}` : 'no halt', s.haltReason ? t.down : t.dim, !!s.haltReason);
|
|
668
|
+
// task-1529: a `run --all` shows its strategies side by side above the shared panels (rows tagged [strategy]).
|
|
669
|
+
const fleet = ui.fleet;
|
|
670
|
+
let fh = 0;
|
|
671
|
+
if (fleet) {
|
|
672
|
+
const lines = fleetCompareTable(fleetCompareRows(fleet));
|
|
673
|
+
fh = Math.min(lines.length + 4, Math.max(5, Math.floor((a.h - sh) * 0.4)));
|
|
674
|
+
const tot = fleetTotals(fleet);
|
|
675
|
+
const g = fleet.global;
|
|
676
|
+
const fb = c.box(a.x, a.y + sh, a.w, fh, { title: `STRATEGIES · ${fleet.members.length} at once · PAPER`, caption: 'one strategy: dexterm autotrade pause|stop|start --strategy ID' });
|
|
677
|
+
const rows = lines.slice(0, Math.max(0, fb.h - 2));
|
|
678
|
+
rows.forEach((l, i) => {
|
|
679
|
+
const m = i ? fleet.members[i - 1] : null;
|
|
680
|
+
const fg = !m ? t.muted : m.state === 'running' ? t.text : m.state === 'paused' ? t.warn : m.state === 'error' || m.state === 'halted' ? t.down : t.dim;
|
|
681
|
+
c.text(fb.x + 1, fb.y + i, fit(l, fb.w - 2), { fg, bg: fb.bg, b: !m });
|
|
682
|
+
});
|
|
683
|
+
const foot = [
|
|
684
|
+
[`together ${usd(tot.pnlUsd)} on $${tot.startUsd.toFixed(2)} · ${tot.closed} closed · ${tot.open} open · simulated fills, not financial advice`, tot.pnlUsd >= 0 ? t.up : t.down],
|
|
685
|
+
[`global: ${g.liveSessions}/${g.caps.maxConcurrentSessions} sessions · $${g.today.modelUsd.toFixed(2)} of $${g.caps.modelUsdPerDay} model spend today${g.load.paused ? ' · machine busy, new sessions wait' : ''}${g.blocked ? ` · ${g.blocked}` : ''}`, g.load.paused || g.blocked ? t.warn : t.muted],
|
|
686
|
+
];
|
|
687
|
+
foot.slice(0, Math.max(0, fb.h - rows.length)).forEach(([l, fg], i) => c.text(fb.x + 1, fb.y + rows.length + i, fit(l, fb.w - 2), { fg, bg: fb.bg }));
|
|
688
|
+
}
|
|
689
|
+
const tag = (x) => { const id = x.strategy; return typeof id === 'string' ? `[${id}] ` : ''; };
|
|
690
|
+
const rest = a.h - sh - fh;
|
|
691
|
+
const ledgerH = Math.max(4, Math.floor(rest * 0.3));
|
|
692
|
+
const upper = rest - ledgerH;
|
|
693
|
+
const third = Math.floor(a.w / 3);
|
|
694
|
+
const panel = (x, w, title, lines) => {
|
|
695
|
+
const b = c.box(x, a.y + sh + fh, w, upper, { title });
|
|
696
|
+
lines.slice(0, b.h).forEach(([l, fg], i) => c.text(b.x + 1, b.y + i, fit(l, b.w - 2), { fg, bg: b.bg }));
|
|
697
|
+
if (!lines.length)
|
|
698
|
+
c.text(b.x + 1, b.y, 'none', { fg: t.dim, bg: b.bg, i: true });
|
|
699
|
+
};
|
|
700
|
+
panel(a.x, third, `CANDIDATES · ${s.candidates.length}`, s.candidates.map((k) => [`${tag(k)}${k.symbol.padEnd(9)} ${fmtAgeSec(k.ageSec).padStart(4)} ${k.matched.join(' · ')}${k.session ? ' ◆ session' : ''}`, t.text]));
|
|
701
|
+
panel(a.x + third, third, `SESSIONS · ${s.sessions.length}`, s.sessions.flatMap((k) => [
|
|
702
|
+
[`${tag(k)}${k.symbol.padEnd(9)} ${String(k.turns).padStart(3)} turns $${k.costUsd.toFixed(2)} of $${k.budgetUsd.toFixed(2)}`, k.costUsd > k.budgetUsd * 0.8 ? t.warn : t.text],
|
|
703
|
+
[` last: ${k.lastDecision ?? '—'}`, t.muted],
|
|
704
|
+
]));
|
|
705
|
+
panel(a.x + 2 * third, a.w - 2 * third, `POSITIONS · ${s.positions.length}`, s.positions.flatMap((p) => [
|
|
706
|
+
[`${tag(p)}${p.symbol.padEnd(8)}${pct(p.pnlPct).padStart(8)}${usd(p.pnlUsd).padStart(9)}${p.freeBag ? ' free bag' : ''}`, p.pnlUsd >= 0 ? t.up : t.down],
|
|
707
|
+
[` ${p.profile} · next: ${p.next ?? '—'}`, t.muted],
|
|
708
|
+
]));
|
|
709
|
+
const lb = c.box(a.x, a.y + sh + fh + upper, a.w, ledgerH, { title: 'RUN LEDGER · newest last', caption: 'hash-chained; every step with its exact inputs' });
|
|
710
|
+
const tail = s.ledgerTail.slice(-lb.h);
|
|
711
|
+
tail.forEach((e, i) => {
|
|
712
|
+
const ago = fmtAgeSec(Math.max(0, Math.round((now - e.t) / 1000)));
|
|
713
|
+
c.text(lb.x + 1, lb.y + i, fit(`#${String(e.seq).padEnd(6)} ${ago.padStart(4)} ${e.kind.padEnd(16)} ${e.text}`, lb.w - 2), { fg: /refus|halt|kill|rug/.test(e.kind) ? t.warn : t.muted, bg: lb.bg });
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function confirmCard(c) {
|
|
717
|
+
const t = c.theme;
|
|
718
|
+
const w = Math.min(64, c.w - 4), h = 6;
|
|
719
|
+
const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
|
|
720
|
+
c.noRain(x, y, w, h);
|
|
721
|
+
const b = c.box(x, y, w, h, { title: 'FLATTEN · PAPER', focus: true, bg: t.panelAlt, border: t.warn });
|
|
722
|
+
c.text(b.x + 1, b.y, fit('Sell every paper position of this runner at the next tick.', b.w - 2), { fg: t.text, bg: b.bg });
|
|
723
|
+
c.text(b.x + 1, b.y + 1, fit('New entries stop first; mechanical exits keep running.', b.w - 2), { fg: t.muted, bg: b.bg });
|
|
724
|
+
c.text(b.x + 1, b.y + 3, 'y flatten · any other key cancels', { fg: t.warn, bg: b.bg, b: true });
|
|
725
|
+
}
|