dexterm-cli 0.1.7 → 0.2.1
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/spectator.js +31 -3
- package/dist/packages/agents/src/tiers.js +12 -0
- package/dist/packages/agents/src/tools.js +13 -5
- package/dist/packages/autotrade/assets/limits.schema.json +569 -0
- package/dist/packages/autotrade/examples/executor/README.md +70 -0
- package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
- package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
- package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
- package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
- package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
- package/dist/packages/autotrade/src/backtest.js +7 -2
- package/dist/packages/autotrade/src/chat/config.js +4 -2
- package/dist/packages/autotrade/src/chat/control.js +1 -0
- package/dist/packages/autotrade/src/chat/notify.js +16 -6
- package/dist/packages/autotrade/src/chat/secrets.js +25 -18
- package/dist/packages/autotrade/src/chat/service.js +3 -0
- package/dist/packages/autotrade/src/engine.js +16 -2
- package/dist/packages/autotrade/src/executor-health.js +217 -0
- package/dist/packages/autotrade/src/fleet.js +5 -2
- package/dist/packages/autotrade/src/forward.js +355 -0
- package/dist/packages/autotrade/src/index.js +21 -0
- package/dist/packages/autotrade/src/journey.js +477 -0
- package/dist/packages/autotrade/src/limits.js +682 -0
- package/dist/packages/autotrade/src/outbox.js +394 -0
- package/dist/packages/autotrade/src/paper-limits.js +87 -0
- package/dist/packages/autotrade/src/real-mode.js +688 -0
- package/dist/packages/autotrade/src/receipts.js +330 -0
- package/dist/packages/autotrade/src/report.js +168 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +194 -28
- package/dist/packages/autotrade/src/service-guard.js +125 -0
- package/dist/packages/autotrade/src/service-install.js +215 -0
- package/dist/packages/autotrade/src/service-recover.js +232 -0
- package/dist/packages/autotrade/src/service.js +289 -0
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/signal-channels.js +312 -0
- package/dist/packages/autotrade/src/signal.js +196 -0
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +15 -2
- package/dist/packages/autotrade/src/trade-ledger.js +2 -2
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +220 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +18 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/analyst/backend.js +3 -2
- package/dist/packages/cli/src/analyst/codex.js +3 -2
- package/dist/packages/cli/src/app-help-gen.js +26 -2
- package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
- package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
- package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
- package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
- package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
- package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
- package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
- package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
- package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
- package/dist/packages/cli/src/autotrade-cmd.js +50 -4
- package/dist/packages/cli/src/classifier-cmd.js +353 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/ledger-cmd.js +4 -2
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/pnl-card.js +4 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
- package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
- package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
- package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/telegram-cmd.js +4 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/data/src/trade/ledger/schema.js +1 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- 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-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- 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 +1 -0
- 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 +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/autotrade-tools.js +168 -5
- package/dist/packages/mcp/src/engine/restrictions.js +3 -3
- package/dist/packages/mcp/src/index.js +1 -1
- package/dist/packages/mcp/src/ledger-tools.js +155 -7
- package/dist/packages/mcp/src/server.js +125 -10
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +12 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
// strategy_backtest observe a strategy or a proposal on recorded history (days <= 14), read-only, rate-limited
|
|
11
11
|
// autotrade_status observe the paper runner's status file, strategies and pending proposals
|
|
12
12
|
//
|
|
13
|
+
// task-1647 (0.2.0 card C), the JOURNEY draft -> backtested -> forward-tested -> always-on -> real:
|
|
14
|
+
//
|
|
15
|
+
// strategy_journey observe the ladder for one strategy: which rungs have evidence on these rules, what is next
|
|
16
|
+
// strategy_forward_report observe a forward test next to the backtest (held-out split) of the same rules
|
|
17
|
+
// strategy_forward_start control start a forward test: live data, paper, the strategy's OWN bankroll, a window
|
|
18
|
+
// (hours <= 72 or closed trades <= 200); only after a backtest; never real
|
|
19
|
+
// strategy_forward_stop control stop one (it flattens and writes its report; an early stop never counts)
|
|
20
|
+
// strategy_promote_propose propose a PROMOTION proposal (always-on or real) with an evidence card, never applied;
|
|
21
|
+
// no rung can be skipped; always-on is the user's y, real is the user's own
|
|
22
|
+
// caps + `signal arm` with a typed phrase on their terminal
|
|
23
|
+
// strategy_backtest now also runs the strategy on each half of the held-out split and records that as the
|
|
24
|
+
// strategy's backtest evidence (the engine's numbers on the recorded tape, never model text).
|
|
25
|
+
//
|
|
13
26
|
// A proposal is a file in `$DEXTERM_HOME/strategies/.proposals/`. Nothing in this module writes a strategy:
|
|
14
27
|
// the user applies a proposal on the Q screen with a keypress (a LOOSER one needs a second keypress, the
|
|
15
28
|
// "less safe" popup), and that path re-checks everything here from scratch (`checkProposal`) instead of trusting
|
|
@@ -19,8 +32,9 @@
|
|
|
19
32
|
//
|
|
20
33
|
// @dexterm/autotrade imports @dexterm/mcp (the firewall), so the server loads the engine lazily (`loadEngine`);
|
|
21
34
|
// the CLI hands its own static import to the same functions.
|
|
35
|
+
import { spawn } from 'node:child_process';
|
|
22
36
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
23
|
-
import { join } from 'node:path';
|
|
37
|
+
import { basename, join } from 'node:path';
|
|
24
38
|
import { randomBytes } from 'node:crypto';
|
|
25
39
|
import { firewallText } from '../../data/src/index.js';
|
|
26
40
|
import { firewall } from "./firewall.js";
|
|
@@ -42,6 +56,21 @@ export const AUTOTRADE_TOOLS = [...STRATEGY_READ_TOOLS, ...STRATEGY_PROPOSE_TOOL
|
|
|
42
56
|
export const PARALLEL_READ_TOOLS = ['autotrade_parallel_list', 'autotrade_parallel_compare'];
|
|
43
57
|
export const PARALLEL_CONTROL_TOOLS = ['autotrade_parallel_start', 'autotrade_parallel_stop'];
|
|
44
58
|
export const PARALLEL_TOOLS = [...PARALLEL_READ_TOOLS, ...PARALLEL_CONTROL_TOOLS];
|
|
59
|
+
/** task-1647: the journey tools. Reads; forward-test control (paper, own bankroll); promotion PROPOSALS (never applied). */
|
|
60
|
+
export const JOURNEY_READ_TOOLS = ['strategy_journey', 'strategy_forward_report'];
|
|
61
|
+
export const JOURNEY_CONTROL_TOOLS = ['strategy_forward_start', 'strategy_forward_stop'];
|
|
62
|
+
export const JOURNEY_PROPOSE_TOOLS = ['strategy_promote_propose'];
|
|
63
|
+
export const JOURNEY_TOOLS = [...JOURNEY_READ_TOOLS, ...JOURNEY_CONTROL_TOOLS, ...JOURNEY_PROPOSE_TOOLS];
|
|
64
|
+
/** Forward tests a model may start per hour (each one is a background process on the user's machine). */
|
|
65
|
+
export const FORWARD_START_RATE = { perHour: 3 };
|
|
66
|
+
export const defaultForwardSpawner = (home, t, env) => {
|
|
67
|
+
const bin = process.argv[1] ?? '';
|
|
68
|
+
if (!/^dexterm(\.m?js)?$/.test(basename(bin)))
|
|
69
|
+
return null;
|
|
70
|
+
const child = spawn(process.execPath, [bin, 'autotrade', 'forward', 'run', t.forwardId], { detached: true, stdio: 'ignore', env: { ...env, DEXTERM_HOME: home } });
|
|
71
|
+
child.unref();
|
|
72
|
+
return child.pid ?? null;
|
|
73
|
+
};
|
|
45
74
|
export const MAX_PROPOSAL_OPS = 20;
|
|
46
75
|
export const BACKTEST_MAX_DAYS = 14;
|
|
47
76
|
/** Backtests: one at a time, at least this far apart, and at most this many per hour per server process. */
|
|
@@ -56,7 +85,7 @@ const DAY_MS = 86_400_000;
|
|
|
56
85
|
* to live, change the book, raise its own daily spend or lower the floor under which a bag counts as stuck.
|
|
57
86
|
*/
|
|
58
87
|
export const STRATEGY_DENIED_FOR_MODELS = [
|
|
59
|
-
{ pointer: '/mode', why: 'only you can change the mode (a model can never switch a strategy to live)' },
|
|
88
|
+
{ pointer: '/mode', why: 'only you can change the mode (a model can never switch a strategy to real or live; real is armed on your own terminal)' },
|
|
60
89
|
{ pointer: '/bankroll', why: 'only you can change the book' },
|
|
61
90
|
{ pointer: '/agent/budgetUsdPerDay', why: 'only you can change the daily spend (a model can never raise its own budget)' },
|
|
62
91
|
{ pointer: '/execution/minExitLiquidityUsd', why: 'only you can change the stuck-bag floor' },
|
|
@@ -690,6 +719,7 @@ export class AutotradeTools {
|
|
|
690
719
|
backtestBusy = false;
|
|
691
720
|
backtestTimes = [];
|
|
692
721
|
proposalTimes = [];
|
|
722
|
+
forwardTimes = [];
|
|
693
723
|
constructor(o = {}) {
|
|
694
724
|
this.opts = o;
|
|
695
725
|
this.env = o.env ?? process.env;
|
|
@@ -710,6 +740,11 @@ export class AutotradeTools {
|
|
|
710
740
|
case 'strategy_propose_new': return await this.propose({ kind: 'new', id: String(args.id ?? ''), from: String(args.fromPreset ?? ''), name: args.name, ops: args.ops ?? [], why: args.why });
|
|
711
741
|
case 'strategy_backtest': return await this.backtest(args);
|
|
712
742
|
case 'autotrade_status': return await this.status();
|
|
743
|
+
case 'strategy_journey': return await this.journey(args);
|
|
744
|
+
case 'strategy_forward_start': return await this.forwardStart(args);
|
|
745
|
+
case 'strategy_forward_stop': return await this.forwardStop(args);
|
|
746
|
+
case 'strategy_forward_report': return await this.forwardReportTool(args);
|
|
747
|
+
case 'strategy_promote_propose': return await this.promotePropose(args);
|
|
713
748
|
case 'autotrade_parallel_list': return await this.parallelList();
|
|
714
749
|
case 'autotrade_parallel_compare': return await this.parallelCompare();
|
|
715
750
|
case 'autotrade_parallel_start': return await this.parallelControl(args, 'start');
|
|
@@ -852,11 +887,21 @@ export class AutotradeTools {
|
|
|
852
887
|
tape = at.tapeFromDoc(at.tapeFromDb(db, { since: until - days * DAY_MS, until, minPeakLiqUsd: 20_000, spanHours: 80 }));
|
|
853
888
|
}
|
|
854
889
|
const results = [];
|
|
890
|
+
let heldOut = null;
|
|
855
891
|
for (const sub of subjects) {
|
|
856
|
-
const
|
|
857
|
-
results.push({ label: sub.label, ...compactReport(report) });
|
|
892
|
+
const full = await at.backtest(sub.s, tape);
|
|
893
|
+
results.push({ label: sub.label, ...compactReport(full.report) });
|
|
894
|
+
// task-1647: a stored strategy's backtest is its journey evidence: the same rules on each half of the split.
|
|
895
|
+
if (args.id !== undefined) {
|
|
896
|
+
const { evidence } = await at.backtestEvidence(sub.s, tape, { by: this.actor, now: t, days, full });
|
|
897
|
+
at.recordBacktestEvidence(this.home(), evidence);
|
|
898
|
+
heldOut = { cutoff: evidence.heldOut.cutoff, train: evidence.heldOut.train, test: evidence.heldOut.test, verdict: evidence.verdict, claim: evidence.claim, rulesHash: evidence.rulesHash };
|
|
899
|
+
}
|
|
858
900
|
}
|
|
859
|
-
return {
|
|
901
|
+
return {
|
|
902
|
+
paper: true, readOnly: true, days, tape: { source: tape.source, mints: tape.mints.length, ticks: tape.ticks.length }, results, ...(heldOut ? { heldOut } : {}), limits: at.DATA_LIMITS,
|
|
903
|
+
note: `A backtest on recorded history with modelled fills, not a prediction. ${heldOut ? 'The held-out split (the newest 30% of coins) is recorded as this strategy\'s backtest evidence for its journey; the strategy itself was not changed. Gain wording only when heldOut.claim is true.' : 'Nothing was saved.'} Quote the trade count: small n means little.`,
|
|
904
|
+
};
|
|
860
905
|
}
|
|
861
906
|
finally {
|
|
862
907
|
this.backtestBusy = false;
|
|
@@ -873,6 +918,124 @@ export class AutotradeTools {
|
|
|
873
918
|
pendingProposals: pending.map((p) => ({ proposalId: p.proposalId, id: p.id, kind: p.kind, riskDelta: p.riskDelta, createdAt: new Date(p.createdAt).toISOString() })),
|
|
874
919
|
};
|
|
875
920
|
}
|
|
921
|
+
// ───────────────────────────── the journey (task-1647) ─────────────────────────────
|
|
922
|
+
async userStrategy(id) {
|
|
923
|
+
const { at } = await this.eng();
|
|
924
|
+
if (typeof id !== 'string' || !at.STRATEGY_ID.test(id))
|
|
925
|
+
return { error: 'id must be a strategy id from strategy_list' };
|
|
926
|
+
const store = await this.store();
|
|
927
|
+
const got = store.get(id);
|
|
928
|
+
if (!got)
|
|
929
|
+
return { error: `no strategy ${id} (strategy_list shows the ids)` };
|
|
930
|
+
if (got.source !== 'user')
|
|
931
|
+
return { error: `${id} is a shipped preset: propose a copy first (strategy_propose_new), the user applies it, then work on the copy` };
|
|
932
|
+
if (!got.ok)
|
|
933
|
+
return { error: `${id} does not validate: ${got.errors.map(at.fmtIssue).join('; ')}` };
|
|
934
|
+
return { s: got.strategy, raw: store.raw(id) };
|
|
935
|
+
}
|
|
936
|
+
async journey(args) {
|
|
937
|
+
const { at } = await this.eng();
|
|
938
|
+
const r = await this.userStrategy(args.id);
|
|
939
|
+
if ('error' in r)
|
|
940
|
+
return r;
|
|
941
|
+
const home = this.home();
|
|
942
|
+
const v = at.journeyView(home, r.s, { armedHash: at.loadArmings(home)[r.s.id]?.strategyHash ?? null });
|
|
943
|
+
const fw = at.findForwardTest(home, r.s.id, this.now());
|
|
944
|
+
const pending = new at.PromotionFiles(home).list({ status: 'pending', id: r.s.id });
|
|
945
|
+
return {
|
|
946
|
+
paper: r.s.mode !== 'real', id: v.id, version: v.version, rulesHash: v.rulesHash, mode: v.mode, stage: v.stage,
|
|
947
|
+
rungs: v.rungs.map((x) => ({ stage: x.stage, ok: x.ok, detail: cleanText(x.detail, 300) })), next: v.next, notes: v.notes.map((n) => cleanText(n, 200)),
|
|
948
|
+
forward: fw ? { forwardId: fw.forwardId, state: at.liveState(fw, this.now()), window: fw.window, progress: fw.progress, counts: fw.rulesHash === v.rulesHash && fw.state === 'done' && fw.source.startsWith('live') } : null,
|
|
949
|
+
pendingPromotions: pending.map((p) => ({ proposalId: p.proposalId, to: p.to, actor: p.actor })),
|
|
950
|
+
note: 'The ladder is draft → backtested → forward-tested → always-on → real, one rung at a time, each on evidence measured on these exact rules. Backtest with strategy_backtest, forward-test with strategy_forward_start, then propose a promotion with strategy_promote_propose; the user applies it. Never claim a result the evidence does not show.',
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
async forwardStart(args) {
|
|
954
|
+
const { at } = await this.eng();
|
|
955
|
+
const r = await this.userStrategy(args.id);
|
|
956
|
+
if ('error' in r)
|
|
957
|
+
return r;
|
|
958
|
+
const home = this.home();
|
|
959
|
+
const t = this.now();
|
|
960
|
+
while (this.forwardTimes.length && t - this.forwardTimes[0] > 3_600_000)
|
|
961
|
+
this.forwardTimes.shift();
|
|
962
|
+
if (this.forwardTimes.length >= FORWARD_START_RATE.perHour)
|
|
963
|
+
return { error: `at most ${FORWARD_START_RATE.perHour} forward tests started per hour`, reason: 'rate-limited' };
|
|
964
|
+
const v = at.journeyView(home, r.s, {});
|
|
965
|
+
if (!v.backtest)
|
|
966
|
+
return { error: `${r.s.id} has no backtest with the held-out split on its current rules: run strategy_backtest {id} first (the ladder is backtest, then forward test)`, reason: 'skip-refused' };
|
|
967
|
+
const hours = args.hours === undefined ? null : Number(args.hours);
|
|
968
|
+
const trades = args.trades === undefined ? null : Number(args.trades);
|
|
969
|
+
// No bankroll, no mode, no source: a model's forward test is always paper, live data, the strategy's own bankroll.
|
|
970
|
+
const c = at.createForwardTest(home, r.s, r.raw, { hours: hours ?? (trades === null ? 24 : null), trades, by: this.actor, now: t, source: 'live' });
|
|
971
|
+
if (!c.ok)
|
|
972
|
+
return { error: cleanText(c.why, 300) };
|
|
973
|
+
const pid = (this.opts.forwardSpawner ?? defaultForwardSpawner)(home, c.test, this.env);
|
|
974
|
+
if (!pid) {
|
|
975
|
+
at.saveForwardTest(home, { ...c.test, state: 'failed', endedAt: t, endReason: 'could not start the host from the chat' });
|
|
976
|
+
return { error: `could not start the forward test from here; the user can start it in a terminal: dexterm autotrade forward start ${r.s.id}${hours ? ` --hours ${hours}` : ''}${trades ? ` --trades ${trades}` : ''}` };
|
|
977
|
+
}
|
|
978
|
+
this.forwardTimes.push(t);
|
|
979
|
+
return {
|
|
980
|
+
paper: true, started: true, forwardId: c.test.forwardId, id: r.s.id, version: c.test.version, window: c.test.window, bankrollUsd: c.test.bankrollUsd,
|
|
981
|
+
note: `A FORWARD TEST runs in the background on LIVE data, on paper, with ${r.s.id}'s own bankroll and its own run file, until its window ends (${at.windowText(c.test.window)}). Read it with strategy_forward_report; stop it with strategy_forward_stop. Say nothing about results until it ends, and then only with the trade counts: under 20 closed trades it proves nothing.`,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
async forwardRef(args) {
|
|
985
|
+
const { at } = await this.eng();
|
|
986
|
+
const ref = args.forwardId !== undefined ? String(args.forwardId) : args.id !== undefined ? String(args.id) : '';
|
|
987
|
+
if (!ref)
|
|
988
|
+
return { error: 'pass id (a strategy id) or forwardId' };
|
|
989
|
+
const t = at.findForwardTest(this.home(), ref, this.now());
|
|
990
|
+
return t ? { t } : { error: `no forward test for ${cleanText(ref, 40)}` };
|
|
991
|
+
}
|
|
992
|
+
async forwardStop(args) {
|
|
993
|
+
const { at } = await this.eng();
|
|
994
|
+
const f = await this.forwardRef(args);
|
|
995
|
+
if ('error' in f)
|
|
996
|
+
return f;
|
|
997
|
+
const r = at.requestForwardStop(this.home(), f.t.forwardId, this.actor, this.now());
|
|
998
|
+
if (!r.ok)
|
|
999
|
+
return { error: cleanText(r.why, 200) };
|
|
1000
|
+
return { paper: true, stopRequested: true, already: r.already, forwardId: f.t.forwardId, note: 'It flattens at its next tick, writes its report and ends. A forward test stopped early is shown in the journey but does not count as the forward-tested rung.' };
|
|
1001
|
+
}
|
|
1002
|
+
async forwardReportTool(args) {
|
|
1003
|
+
const { at } = await this.eng();
|
|
1004
|
+
const f = await this.forwardRef(args);
|
|
1005
|
+
if ('error' in f)
|
|
1006
|
+
return f;
|
|
1007
|
+
const r = at.forwardReport(this.home(), f.t, this.now());
|
|
1008
|
+
return {
|
|
1009
|
+
paper: true, readOnly: true, forwardId: r.forwardId, id: r.strategyId, version: r.version, state: r.state, source: r.source, window: r.window, elapsed: r.elapsed,
|
|
1010
|
+
endReason: r.endReason ? cleanText(r.endReason, 200) : null, progress: r.progress, metrics: r.metrics,
|
|
1011
|
+
comparison: { rows: r.comparison.rows, verdict: r.comparison.verdict, claim: r.comparison.claim, sampleOk: r.comparison.sampleOk },
|
|
1012
|
+
note: r.comparison.claim ? 'Both halves of the evidence agree; still quote the trade counts and say it is paper with modelled fills.' : 'No performance claim: report the numbers with their trade counts and the verdict lines as they are. Paper, modelled fills, not financial advice.',
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async promotePropose(args) {
|
|
1016
|
+
const { at } = await this.eng();
|
|
1017
|
+
const r = await this.userStrategy(args.id);
|
|
1018
|
+
if ('error' in r)
|
|
1019
|
+
return r;
|
|
1020
|
+
const t = this.now();
|
|
1021
|
+
while (this.proposalTimes.length && t - this.proposalTimes[0] > 600_000)
|
|
1022
|
+
this.proposalTimes.shift();
|
|
1023
|
+
if (this.proposalTimes.length >= 10)
|
|
1024
|
+
return { error: 'proposal rate limit: at most 10 proposals per 10 minutes', reason: 'rate-limited' };
|
|
1025
|
+
const home = this.home();
|
|
1026
|
+
const p = at.proposePromotion(home, r.s, String(args.to ?? ''), { actor: this.actor, why: cleanText(args.why), now: t, armedHash: at.loadArmings(home)[r.s.id]?.strategyHash ?? null });
|
|
1027
|
+
if (!p.ok)
|
|
1028
|
+
return { ok: false, applied: false, reason: p.reason, error: cleanText(p.why, 300), note: p.reason === 'skip-refused' ? 'No rung can be skipped: do the missing step first (strategy_journey says which).' : p.reason === 'evidence-floor' ? 'Nothing was proposed. Real needs the evidence floor: a COMPLETED forward test of these exact rules with the user\'s minimum of closed paper trades, and a backtest on record. The floor is the user\'s limit: you cannot propose past it, lower it, skip it or override it. Say what is missing and stop.' : 'Nothing was proposed.' };
|
|
1029
|
+
if (!p.duplicate)
|
|
1030
|
+
this.proposalTimes.push(t);
|
|
1031
|
+
const pr = p.proposal;
|
|
1032
|
+
return {
|
|
1033
|
+
ok: true, applied: false, proposalId: pr.proposalId, id: pr.id, to: pr.to, version: pr.version, card: pr.card, duplicate: p.duplicate,
|
|
1034
|
+
note: pr.to === 'real'
|
|
1035
|
+
? `This is a PROPOSAL and it is NOT applied. Real mode is the user's alone: they set their own caps (dexterm autotrade signal caps set) and arm it on their own terminal with a typed phrase (dexterm autotrade promote apply ${pr.proposalId}, which hands over to signal arm). You cannot apply it, set or suggest cap values, or arm anything.`
|
|
1036
|
+
: `This is a PROPOSAL and it is NOT applied. The user applies it with y on a real terminal (dexterm autotrade promote apply ${pr.proposalId}) or on the Q screen journey view; then the always-on service runs it on paper. You cannot apply it.`,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
876
1039
|
// ───────────────────────────── parallel strategies (task-1529) ─────────────────────────────
|
|
877
1040
|
/** autotrade.json's strategies and, while `run --all` runs, each one's state. */
|
|
878
1041
|
async parallelList() {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// never raise a cap, arm a mandate or turn live on, and no tool, MCP call or chat message changes the profile.
|
|
15
15
|
import { ANALYST_ACTION_TYPES } from '../../../data/src/index.js';
|
|
16
16
|
import { PAPER_NOTE_TOOLS, PAPER_READ_TOOLS, PAPER_WRITE_TOOLS, READ_ONLY_TOOLS } from "../server.js";
|
|
17
|
-
import { PARALLEL_CONTROL_TOOLS, STRATEGY_PROPOSE_TOOLS } from "../autotrade-tools.js";
|
|
17
|
+
import { JOURNEY_CONTROL_TOOLS, JOURNEY_PROPOSE_TOOLS, PARALLEL_CONTROL_TOOLS, STRATEGY_PROPOSE_TOOLS } from "../autotrade-tools.js";
|
|
18
18
|
import { LEDGER_PROPOSE_TOOLS } from "../ledger-tools.js";
|
|
19
19
|
export const RESTRICTION_LEVELS = ['observe', 'propose', 'paper', 'full'];
|
|
20
20
|
export const RESTRICTION_RANK = { observe: 0, propose: 1, paper: 2, full: 3 };
|
|
@@ -65,7 +65,7 @@ export function engineToolAllowlist(level, actionTypes) {
|
|
|
65
65
|
if (!actionTypes || actionTypes.includes(t))
|
|
66
66
|
out.add(t);
|
|
67
67
|
if (allows(level, 'strategy-proposals') && !observeOnly)
|
|
68
|
-
for (const t of STRATEGY_PROPOSE_TOOLS)
|
|
68
|
+
for (const t of [...STRATEGY_PROPOSE_TOOLS, ...JOURNEY_PROPOSE_TOOLS])
|
|
69
69
|
out.add(t);
|
|
70
70
|
// Paper tools: the MCP server still refuses them unless the user armed a mandate and enabled agent paper trading.
|
|
71
71
|
if (allows(level, 'paper-orders') && !observeOnly)
|
|
@@ -74,7 +74,7 @@ export function engineToolAllowlist(level, actionTypes) {
|
|
|
74
74
|
// v0.1.7 integration: the other paper-side writes (start / stop / pause one listed parallel strategy, a paper
|
|
75
75
|
// journal tag, a backtest request file) join at `paper`, so `paper` and `full` offer every tool the claude path has
|
|
76
76
|
if (allows(level, 'paper-orders') && !observeOnly)
|
|
77
|
-
for (const t of [...PARALLEL_CONTROL_TOOLS, ...PAPER_NOTE_TOOLS, ...LEDGER_PROPOSE_TOOLS])
|
|
77
|
+
for (const t of [...PARALLEL_CONTROL_TOOLS, ...JOURNEY_CONTROL_TOOLS, ...PAPER_NOTE_TOOLS, ...LEDGER_PROPOSE_TOOLS])
|
|
78
78
|
out.add(t);
|
|
79
79
|
return [...out].filter((n) => !NEVER_TOOL.test(n));
|
|
80
80
|
}
|
|
@@ -7,7 +7,7 @@ export { resolvePaperAgentDesk, paperAgentDeskDb } from "./paper-agent.js";
|
|
|
7
7
|
export { OverlayStore, mergeOverlay, loadBundled, checkKb, checkSkill, moneyToolMentions, isMoneyToolName, parseFrontmatter, dextermHome, userKbDir, userSkillsDir, userDir, contentJournalPath, journalContent, writeOverlayFile, resetOverlayFile, templateFor, KB_LIMITS, SKILL_LIMITS, KB_ID, SKILL_NAME, KB_RULE, MONEY_TOOLS, } from "./user-content.js";
|
|
8
8
|
// task-1507: the AUTOTRADE chat tools (strategy reads + proposals), the model deny-list, the risk delta and the
|
|
9
9
|
// proposal files the Q screen's keypress path re-checks and applies.
|
|
10
|
-
export { AutotradeTools, ProposalFiles, checkProposal, riskDelta, diffLines, screenDelta, deniedChanges, opsPolicyErrors, opsShapeErrors, proposalCardView, proposalsDir, isProposalId, cleanText, loadEngine, STRATEGY_DENIED_FOR_MODELS, STRATEGY_READ_TOOLS, STRATEGY_PROPOSE_TOOLS, AUTOTRADE_TOOLS, PARALLEL_READ_TOOLS, PARALLEL_CONTROL_TOOLS, PARALLEL_TOOLS, MAX_PROPOSAL_OPS, BACKTEST_MAX_DAYS, BACKTEST_RATE, PROPOSAL_SCHEMA, RUNNER_STATUS_SCHEMA, } from "./autotrade-tools.js";
|
|
10
|
+
export { AutotradeTools, ProposalFiles, checkProposal, riskDelta, diffLines, screenDelta, deniedChanges, opsPolicyErrors, opsShapeErrors, proposalCardView, proposalsDir, isProposalId, cleanText, loadEngine, STRATEGY_DENIED_FOR_MODELS, STRATEGY_READ_TOOLS, STRATEGY_PROPOSE_TOOLS, AUTOTRADE_TOOLS, PARALLEL_READ_TOOLS, PARALLEL_CONTROL_TOOLS, PARALLEL_TOOLS, JOURNEY_READ_TOOLS, JOURNEY_CONTROL_TOOLS, JOURNEY_PROPOSE_TOOLS, JOURNEY_TOOLS, FORWARD_START_RATE, defaultForwardSpawner, MAX_PROPOSAL_OPS, BACKTEST_MAX_DAYS, BACKTEST_RATE, PROPOSAL_SCHEMA, RUNNER_STATUS_SCHEMA, } from "./autotrade-tools.js";
|
|
11
11
|
export { buildDossier } from "./dossier.js";
|
|
12
12
|
// task-1530: the trade-ledger tools (reads + the backtest request), and the request files the CLI runs or dismisses.
|
|
13
13
|
export { LedgerTools, LEDGER_TOOLS, LEDGER_READ_TOOLS, LEDGER_PROPOSE_TOOLS, tradeView, filterFromArgs } from "./ledger-tools.js";
|
|
@@ -11,9 +11,17 @@
|
|
|
11
11
|
// every candidate of a run (what the skips turned into)
|
|
12
12
|
// telegram_status observe the Telegram connector's settings: enabled, paired chats (a count), approvals, notify
|
|
13
13
|
// trading_status observe where live trading happens (task-1571: not in dexterm; it holds no key and places no
|
|
14
|
-
// live order)
|
|
14
|
+
// live order), whether the panic switch is engaged, and real mode's state (task-1645)
|
|
15
|
+
// trade_fire_feed observe task-1645: the MCP channel of real mode: the LIVE TRADE FIRE signals of the feed
|
|
16
|
+
// (autotrade/real/mcp-feed.jsonl) after a nonce, and real mode's status. Reading is all it
|
|
17
|
+
// does: no tool arms real mode, changes a cap, lifts the kill switch or records a receipt
|
|
18
|
+
// trading_limits observe task-1654: real mode's limits in force (for a strategy / account / mode), where each
|
|
19
|
+
// number comes from, what each limit does, today's counters and the executor's health
|
|
15
20
|
// backtest_requests observe the backtests this agent asked for, and their results once the user ran them
|
|
16
21
|
// backtest_request propose ask the user to run a longer backtest (a request file; the user runs or dismisses it)
|
|
22
|
+
// limits_propose propose task-1654: a limits change as a DIFF CARD (old -> new, which numbers it raises, the
|
|
23
|
+
// exact command the user would type). It writes nothing at all: only the user applies a
|
|
24
|
+
// change, on their own terminal (`dexterm autotrade limits set`; a raise is typed)
|
|
17
25
|
//
|
|
18
26
|
// Nothing here can trade, sign, change a cap, enable live or touch a key: the ledger is read through
|
|
19
27
|
// `@dexterm/autotrade`'s loader (pure file reads, loaded lazily: @dexterm/autotrade imports this package). Text that
|
|
@@ -26,10 +34,10 @@ import { dextermHome } from "./user-content.js";
|
|
|
26
34
|
import { BacktestRequests, MAX_PENDING_REQUESTS, REQUEST_MAX_DAYS, REQUEST_RATE_PER_HOUR } from "./backtest-requests.js";
|
|
27
35
|
export const LEDGER_READ_TOOLS = [
|
|
28
36
|
'ledger_trades', 'ledger_positions', 'ledger_equity', 'ledger_summary', 'ledger_compare',
|
|
29
|
-
'candidate_dossier', 'outcomes_lookup', 'telegram_status', 'trading_status', 'backtest_requests',
|
|
37
|
+
'candidate_dossier', 'outcomes_lookup', 'telegram_status', 'trading_status', 'trade_fire_feed', 'trading_limits', 'backtest_requests',
|
|
30
38
|
];
|
|
31
39
|
/** Write-shaped (a request file), so not on the read-only surface a skill may name. */
|
|
32
|
-
export const LEDGER_PROPOSE_TOOLS = ['backtest_request'];
|
|
40
|
+
export const LEDGER_PROPOSE_TOOLS = ['backtest_request', 'limits_propose'];
|
|
33
41
|
export const LEDGER_TOOLS = [...LEDGER_READ_TOOLS, ...LEDGER_PROPOSE_TOOLS];
|
|
34
42
|
const MINT = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
35
43
|
const RUN_ID = /^[0-9TZ]+-[a-z][a-z0-9-]{1,31}-[0-9a-f]{4}$/;
|
|
@@ -64,10 +72,10 @@ export function filterFromArgs(a, now) {
|
|
|
64
72
|
return allowed.includes(a[k]) ? a[k] : false;
|
|
65
73
|
};
|
|
66
74
|
const source = pick('source', ['paper', 'desk', 'autotrade', 'live']);
|
|
67
|
-
const mode = pick('mode', ['paper', 'devnet', 'live']);
|
|
75
|
+
const mode = pick('mode', ['paper', 'devnet', 'live', 'real']);
|
|
68
76
|
const status = pick('status', ['open', 'closed']);
|
|
69
77
|
if (source === false || mode === false || status === false)
|
|
70
|
-
return { ok: false, error: 'source is paper|desk|autotrade|live, mode is paper|devnet|live, status is open|closed' };
|
|
78
|
+
return { ok: false, error: 'source is paper|desk|autotrade|live, mode is paper|devnet|live|real, status is open|closed' };
|
|
71
79
|
if (source)
|
|
72
80
|
f.source = source;
|
|
73
81
|
if (mode)
|
|
@@ -135,6 +143,9 @@ export class LedgerTools {
|
|
|
135
143
|
case 'outcomes_lookup': return await this.outcomes(args);
|
|
136
144
|
case 'telegram_status': return this.telegram();
|
|
137
145
|
case 'trading_status': return this.trading();
|
|
146
|
+
case 'trade_fire_feed': return this.signals(args);
|
|
147
|
+
case 'trading_limits': return await this.limits(args);
|
|
148
|
+
case 'limits_propose': return await this.limitsPropose(args);
|
|
138
149
|
case 'backtest_requests': return this.requests();
|
|
139
150
|
case 'backtest_request': return await this.request(args);
|
|
140
151
|
default: return { error: `unknown ledger tool ${name}` };
|
|
@@ -154,7 +165,7 @@ export class LedgerTools {
|
|
|
154
165
|
const rows = filterTrades(v.trades, filter);
|
|
155
166
|
const integrity = { ledger: v.file.check.ok ? `verified, ${v.file.events.length} events` : `FAILS its check at line ${v.file.check.at}: ${v.file.check.error}`, pendingFromSources: v.pending.length, problems: v.sources.filter((s) => s.problem).map((s) => s.problem) };
|
|
156
167
|
const modes = [...new Set(rows.map((t) => t.walletMode))];
|
|
157
|
-
const base = { readOnly: true, modes, integrity, note: modes.length > 1 ? 'PAPER, DEVNET and LIVE trades are in this set: filter by mode before comparing results.' : modes.includes('paper') ? 'PAPER (simulated) trades: never present them as results.' : undefined };
|
|
168
|
+
const base = { readOnly: true, modes, integrity, note: modes.length > 1 ? 'PAPER, REAL, DEVNET and LIVE trades are in this set: filter by mode before comparing results.' : modes.includes('paper') ? 'PAPER (simulated) trades: never present them as results.' : modes.includes('real') ? 'REAL-mode trades: dexterm\'s model of the trades it signalled to the user\'s executor; the executor\'s receipts are the real fills.' : undefined };
|
|
158
169
|
if (name === 'ledger_trades' || name === 'ledger_positions') {
|
|
159
170
|
const limit = Math.min(200, Math.max(1, Number.isInteger(args.limit) ? Number(args.limit) : 50));
|
|
160
171
|
const newest = [...rows].sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
@@ -292,13 +303,150 @@ export class LedgerTools {
|
|
|
292
303
|
// ───────────────────────────── live trading (task-1571: not in dexterm) ───────────────
|
|
293
304
|
trading() {
|
|
294
305
|
const panic = existsSync(join(this.home(), 'trading.panic'));
|
|
306
|
+
const real = this.realState();
|
|
295
307
|
return {
|
|
296
308
|
readOnly: true,
|
|
297
|
-
liveTrading: 'not in dexterm: dexterm holds no key, signs nothing and places no live order. Live trading happens in the Telegram trading bot the user chose, which holds its own wallet; dexterm only sends alerts
|
|
309
|
+
liveTrading: 'not in dexterm: dexterm holds no key, signs nothing and places no live order. Live trading happens in the Telegram trading bot (or script) the user chose, which holds its own wallet; dexterm only sends alerts, run control and, in real mode, LIVE TRADE FIRE signals to the user\'s own executor (telegram_status, trade_fire_feed).',
|
|
298
310
|
paperTrading: 'local: the paper ticket, paper accounts and paper autotrade runs (ledger_* tools)',
|
|
311
|
+
realMode: `${real.armed.length} strategy(ies) armed for real mode by the user on their own terminal${real.kill ? `; the real-mode kill switch is ON (${real.kill})` : ''}. Only the user arms, disarms, sets caps or lifts the kill switch, on their own terminal; no tool can.`,
|
|
299
312
|
panicSwitch: panic ? 'engaged (autotrade runs and the paper agent desk are halted; only `dexterm wallet resume` in a terminal clears it)' : 'off',
|
|
300
313
|
};
|
|
301
314
|
}
|
|
315
|
+
// ───────────────────────────── real mode (task-1645) ─────────────────────────────
|
|
316
|
+
readJson(path) {
|
|
317
|
+
try {
|
|
318
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Real mode's state from its files (pure reads; ids and numbers only). */
|
|
325
|
+
realState() {
|
|
326
|
+
const dir = join(this.home(), 'autotrade', 'real');
|
|
327
|
+
const arming = this.readJson(join(dir, 'arming.json'));
|
|
328
|
+
const armed = Object.values((arming?.armed ?? {}))
|
|
329
|
+
.filter((a) => typeof a.strategyId === 'string' && /^[a-z][a-z0-9-]{1,31}$/.test(a.strategyId))
|
|
330
|
+
.map((a) => ({ id: String(a.strategyId), version: String(a.version ?? '').slice(0, 16), accountId: String(a.accountId ?? '').replace(/[^a-z0-9_-]/g, '').slice(0, 32) }));
|
|
331
|
+
const killFile = join(dir, 'kill.json');
|
|
332
|
+
const k = existsSync(killFile) ? this.readJson(killFile) : null;
|
|
333
|
+
// task-1654: the caps are four of the limits (limits.json global, else card A's caps.json).
|
|
334
|
+
const lim = this.readJson(join(dir, 'limits.json'))?.global;
|
|
335
|
+
const caps = (lim ? Object.fromEntries(['perTradeSol', 'dailySol', 'buysPerDay', 'maxOpenPositions'].filter((k) => typeof lim[k] === 'number').map((k) => [k, lim[k]])) : undefined) ?? this.readJson(join(dir, 'caps.json'))?.caps;
|
|
336
|
+
const capsNum = caps ? Object.fromEntries(Object.entries(caps).filter(([, v]) => typeof v === 'number')) : null;
|
|
337
|
+
const n = Number(this.readJson(join(dir, 'nonce.json'))?.last);
|
|
338
|
+
return { armed, kill: existsSync(killFile) ? cleanLedgerText(k?.reason ?? 'engaged', 80) || 'engaged' : null, caps: capsNum, lastNonce: Number.isSafeInteger(n) ? n : null };
|
|
339
|
+
}
|
|
340
|
+
signals(args) {
|
|
341
|
+
const after = Number(args.after_nonce ?? 0);
|
|
342
|
+
const limit = Number(args.limit ?? 50);
|
|
343
|
+
if (!Number.isSafeInteger(after) || after < 0)
|
|
344
|
+
return { error: 'after_nonce is a whole number >= 0' };
|
|
345
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
346
|
+
return { error: 'limit is a whole number from 1 to 100' };
|
|
347
|
+
const feed = join(this.home(), 'autotrade', 'real', 'mcp-feed.jsonl');
|
|
348
|
+
const out = [];
|
|
349
|
+
let feedOn = false;
|
|
350
|
+
if (existsSync(feed)) {
|
|
351
|
+
feedOn = true;
|
|
352
|
+
for (const l of readFileSync(feed, 'utf8').split('\n')) {
|
|
353
|
+
if (!l.trim())
|
|
354
|
+
continue;
|
|
355
|
+
let x;
|
|
356
|
+
try {
|
|
357
|
+
x = JSON.parse(l);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (x.schema !== 'dexterm-trade-fire/1' || typeof x.nonce !== 'number' || x.nonce <= after)
|
|
363
|
+
continue;
|
|
364
|
+
// The signal as dexterm wrote it; the only token text in it is the symbol, already reduced to [A-Za-z0-9._$-].
|
|
365
|
+
out.push({ ...x, symbol: typeof x.symbol === 'string' ? x.symbol.replace(/[^A-Za-z0-9._$-]/g, '').slice(0, 16) : undefined });
|
|
366
|
+
if (out.length >= limit)
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const st = this.realState();
|
|
371
|
+
return {
|
|
372
|
+
readOnly: true, schema: 'dexterm-trade-fire/1', signals: out, nextAfterNonce: out.length ? out[out.length - 1].nonce : after,
|
|
373
|
+
realMode: { armed: st.armed, kill: st.kill, caps: st.caps, lastNonce: st.lastNonce, mcpFeed: feedOn ? 'on' : 'off (the user enables it: dexterm autotrade signal channels --mcp on)' },
|
|
374
|
+
rules: 'An executor acts only on its own accountId, ignores a signal after expiresAt, and dedupes by nonce (delivery is at-least-once). type "halted" ends a run; type "test" is never traded. The user records what was done with `dexterm autotrade receipt`; this tool changes nothing.',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// ───────────────────────────── limits (task-1654) ─────────────────────────────
|
|
378
|
+
limitsContext(args) {
|
|
379
|
+
const id = (v, name) => (v === undefined ? '-' : typeof v === 'string' && /^[a-z0-9][a-z0-9._-]{0,63}$/.test(v) ? v : { error: `${name} is an id ([a-z0-9._-], 64 max)` });
|
|
380
|
+
const strategyId = id(args.strategy, 'strategy');
|
|
381
|
+
if (typeof strategyId !== 'string')
|
|
382
|
+
return strategyId;
|
|
383
|
+
const accountId = id(args.account, 'account');
|
|
384
|
+
if (typeof accountId !== 'string')
|
|
385
|
+
return accountId;
|
|
386
|
+
if (args.mode !== undefined && args.mode !== 'real' && args.mode !== 'paper')
|
|
387
|
+
return { error: 'mode is real or paper' };
|
|
388
|
+
return { strategyId, accountId, mode: args.mode === 'paper' ? 'paper' : 'real' };
|
|
389
|
+
}
|
|
390
|
+
async limits(args) {
|
|
391
|
+
const c = this.limitsContext(args);
|
|
392
|
+
if ('error' in c)
|
|
393
|
+
return c;
|
|
394
|
+
const at = await import('../../autotrade/src/index.js');
|
|
395
|
+
const home = this.home();
|
|
396
|
+
const l = at.loadLimits(home);
|
|
397
|
+
const doc = l.ok ? l.doc : null;
|
|
398
|
+
const e = at.effectiveLimits(doc, c);
|
|
399
|
+
const risk = at.loadRiskState(home, this.now());
|
|
400
|
+
const ex = at.executorHealth(home, { now: this.now(), windowMin: e.values.executorSilenceMin ?? null });
|
|
401
|
+
return {
|
|
402
|
+
readOnly: true, context: c, applies: e.applies,
|
|
403
|
+
state: l.ok ? (l.source === 'migrated' ? 'migrated from caps.json' : l.unjournaled ? 'changed outside dexterm (a real run refuses it if it raises anything)' : 'saved by the user') : l.missing ? 'not set yet: placeholder defaults (real mode does not arm on them silently)' : 'unreadable: real buys are refused',
|
|
404
|
+
limits: at.LIMIT_FIELDS.map((f) => ({ key: f.key, group: f.group, unit: f.unit, kind: f.dir === 'max' ? 'cap (smaller is stricter)' : 'floor (larger is stricter)', value: e.values[f.key] ?? null, from: e.from[f.key] ?? null, placeholder: at.DEFAULT_LIMITS[f.key] ?? null, what: f.help })),
|
|
405
|
+
today: { day: risk.day, buys: risk.buys, buySol: risk.buySol, realizedSol: risk.dayPnlSol, lossStreak: risk.lossStreak, pausedUntil: iso(risk.pausedUntil), breaker: risk.breaker ? { at: iso(risk.breaker.at), drawdownSol: risk.breaker.drawdownSol } : null },
|
|
406
|
+
executor: at.healthLine(ex),
|
|
407
|
+
rules: 'Limits live outside the model. The effective number is the placeholder default replaced by the user\'s global value, then the STRICTER of every scope that sets it (mode, strategy, account). No tool changes them: limits_propose returns a diff card for the user to apply on their own terminal (raising needs their typed confirmation). The defaults are placeholders, not advice.',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async limitsPropose(args) {
|
|
411
|
+
const at = await import('../../autotrade/src/index.js');
|
|
412
|
+
const scope = typeof args.scope === 'string' ? args.scope : 'global';
|
|
413
|
+
const changes = args.changes;
|
|
414
|
+
if (!changes || typeof changes !== 'object' || Array.isArray(changes) || !Object.keys(changes).length || Object.keys(changes).length > 24)
|
|
415
|
+
return { error: 'changes is an object of 1 to 24 limits: { "perTradeSol": 0.1, "dailyLossSol": null }' };
|
|
416
|
+
// The agent's own reason is returned apart from the card, firewalled: the card is engine-made lines only.
|
|
417
|
+
const why = typeof args.why === 'string' ? fw(args.why.slice(0, 200), 'limits-why') : null;
|
|
418
|
+
const home = this.home();
|
|
419
|
+
const l = at.loadLimits(home);
|
|
420
|
+
const cur = l.ok ? l.doc : at.emptyLimits();
|
|
421
|
+
const next = structuredClone(cur);
|
|
422
|
+
delete next.migratedFrom;
|
|
423
|
+
const ref = at.scopeRef(next, scope, true);
|
|
424
|
+
if ('error' in ref)
|
|
425
|
+
return { error: ref.error };
|
|
426
|
+
for (const [k, v] of Object.entries(changes)) {
|
|
427
|
+
if (!at.limitField(k))
|
|
428
|
+
return { error: `unknown limit "${String(k).slice(0, 40)}"` };
|
|
429
|
+
if (v !== null && (typeof v !== 'number' || !Number.isFinite(v)))
|
|
430
|
+
return { error: `${k}: a number, or null to remove it` };
|
|
431
|
+
if (v === null && !ref.isGlobal)
|
|
432
|
+
delete ref.set[k];
|
|
433
|
+
else
|
|
434
|
+
ref.set[k] = v;
|
|
435
|
+
}
|
|
436
|
+
const issues = at.validateLimits(next);
|
|
437
|
+
if (issues.length)
|
|
438
|
+
return { error: issues.map((i) => `${i.path}: ${i.msg}`).join('; ').slice(0, 400) };
|
|
439
|
+
const diff = at.diffLimits(l.ok ? cur : null, next);
|
|
440
|
+
const up = at.raises(diff);
|
|
441
|
+
const assign = Object.entries(changes).map(([k, v]) => `${k}=${v === null ? 'off' : v}`).join(' ');
|
|
442
|
+
return {
|
|
443
|
+
applied: false, proposal: true,
|
|
444
|
+
card: [`PROPOSED LIMITS CHANGE · ${scope}`, ...diff.map((d) => ` ${at.changeLine(d)}`), up.length ? ` raises ${up.length} limit(s): you would type the confirmation on your own terminal` : ' lowers only: applies at once when you run it', ` to apply it yourself: dexterm autotrade limits set --scope ${scope} ${assign}`],
|
|
445
|
+
changes: diff, raises: up.length, why,
|
|
446
|
+
command: `dexterm autotrade limits set --scope ${scope} ${assign}`,
|
|
447
|
+
note: 'Nothing was changed: no tool can change a limit. Only the user applies this, on their own terminal.',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
302
450
|
// ───────────────────────────── backtest requests ─────────────────────────────
|
|
303
451
|
requests() {
|
|
304
452
|
const rs = new BacktestRequests(this.home()).list().slice(0, 20);
|