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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// dexterm — the AUTOTRADE screen's JOURNEY view (`Q`, then `J` on a strategy; 0.2.0 card C). One strategy's ladder:
|
|
2
|
+
// draft → backtested → forward-tested → always-on (paper) → real, with the last backtest (held-out split), the forward
|
|
3
|
+
// test (running or its result next to the backtest), the always-on service membership and the mode.
|
|
4
|
+
//
|
|
5
|
+
// Keys on the view: `b` backtests (recorded as evidence), `F` starts a 24 h forward test (paper, live data, the
|
|
6
|
+
// strategy's own bankroll, in the background), `X` stops it, `A` opens the pending ALWAYS-ON promotion's card (y
|
|
7
|
+
// applies it: autotrade.json gets the paper member). A promotion to REAL is never applied here: it needs the user's own
|
|
8
|
+
// caps and the typed phrase on a terminal (`dexterm autotrade promote apply <pp-id>`). Nothing here reaches a wallet,
|
|
9
|
+
// a key, a mandate or the signer.
|
|
10
|
+
import { dirname } from 'node:path';
|
|
11
|
+
import { PromotionFiles, applyAlwaysOn, createForwardTest, findForwardTest, forwardReport, journeyView, liveState, loadArmings, requestForwardStop, saveForwardTest, windowText, } from '../../../autotrade/src/index.js';
|
|
12
|
+
import { spawnForwardHost } from "../autotrade-cmd/forward.js";
|
|
13
|
+
export const defaultForwardSpawn = (home, t) => spawnForwardHost(home, t, process.env);
|
|
14
|
+
const homeOf = (ui) => dirname(ui.deps.store.dir);
|
|
15
|
+
export function openJourney(ui, id) {
|
|
16
|
+
if (!id)
|
|
17
|
+
return 'no strategy selected';
|
|
18
|
+
ui.journey = { id, view: null, error: null, forward: null, report: null, pending: [], readAt: 0 };
|
|
19
|
+
ui.view = 'journey';
|
|
20
|
+
refreshJourney(ui, true);
|
|
21
|
+
return ui.journey.error;
|
|
22
|
+
}
|
|
23
|
+
/** Re-read the journey (at most every 2 s unless forced): evidence, forward test, pending promotions. */
|
|
24
|
+
export function refreshJourney(ui, force = false) {
|
|
25
|
+
const j = ui.journey;
|
|
26
|
+
if (!j)
|
|
27
|
+
return;
|
|
28
|
+
const now = ui.deps.now();
|
|
29
|
+
if (!force && now - j.readAt < 2000)
|
|
30
|
+
return;
|
|
31
|
+
j.readAt = now;
|
|
32
|
+
const home = homeOf(ui);
|
|
33
|
+
const got = ui.deps.store.get(j.id);
|
|
34
|
+
if (!got || !got.ok) {
|
|
35
|
+
j.view = null;
|
|
36
|
+
j.error = got ? `${j.id} does not validate` : `no strategy ${j.id}`;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
j.view = journeyView(home, got.strategy, { armedHash: loadArmings(home)[j.id]?.strategyHash ?? null });
|
|
41
|
+
j.forward = findForwardTest(home, j.id, now);
|
|
42
|
+
j.report = j.forward ? forwardReport(home, j.forward, now) : null;
|
|
43
|
+
j.pending = new PromotionFiles(home).list({ status: 'pending', id: j.id });
|
|
44
|
+
j.error = null;
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
j.error = `journey unreadable: ${e.message}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** `F`: a 24 h forward test of this version, on paper, live data, its own bankroll, started in the background. */
|
|
51
|
+
export function journeyForward(ui) {
|
|
52
|
+
const j = ui.journey;
|
|
53
|
+
if (!j)
|
|
54
|
+
return 'F starts a forward test from the journey view (J)';
|
|
55
|
+
const got = ui.deps.store.get(j.id);
|
|
56
|
+
if (!got || !got.ok)
|
|
57
|
+
return `no valid strategy ${j.id}`;
|
|
58
|
+
if (got.source !== 'user')
|
|
59
|
+
return `${j.id} is a preset: copy it first (d)`;
|
|
60
|
+
const home = homeOf(ui);
|
|
61
|
+
const c = createForwardTest(home, got.strategy, ui.deps.store.raw(j.id), { hours: 24, by: 'user:tui', now: ui.deps.now() });
|
|
62
|
+
if (!c.ok)
|
|
63
|
+
return c.why;
|
|
64
|
+
const pid = (ui.deps.forwardSpawn ?? defaultForwardSpawn)(home, c.test);
|
|
65
|
+
if (!pid) {
|
|
66
|
+
saveForwardTest(home, { ...c.test, state: 'failed', endedAt: ui.deps.now(), endReason: 'could not start the host' });
|
|
67
|
+
return `could not start it; in a terminal: dexterm autotrade forward start ${j.id}`;
|
|
68
|
+
}
|
|
69
|
+
refreshJourney(ui, true);
|
|
70
|
+
return `forward test ${c.test.forwardId} started · ${windowText(c.test.window)} · paper, live data, own bankroll $${c.test.bankrollUsd} · X stops it`;
|
|
71
|
+
}
|
|
72
|
+
/** `X`: stop the running forward test (it flattens and writes its report; an early stop does not count). */
|
|
73
|
+
export function journeyStopForward(ui) {
|
|
74
|
+
const j = ui.journey;
|
|
75
|
+
if (!j)
|
|
76
|
+
return 'X stops a forward test from the journey view (J)';
|
|
77
|
+
const t = findForwardTest(homeOf(ui), j.id, ui.deps.now());
|
|
78
|
+
if (!t)
|
|
79
|
+
return 'no forward test';
|
|
80
|
+
const r = requestForwardStop(homeOf(ui), t.forwardId, 'tui', ui.deps.now());
|
|
81
|
+
refreshJourney(ui, true);
|
|
82
|
+
return r.ok ? `${r.already ? 'already stopping' : 'stop requested'}: ${t.forwardId} flattens and writes its report (an early stop does not count)` : r.why;
|
|
83
|
+
}
|
|
84
|
+
/** `A`: open the pending always-on promotion's confirm card (y applies). Real is a terminal-only step. */
|
|
85
|
+
export function journeyPromote(ui) {
|
|
86
|
+
const j = ui.journey;
|
|
87
|
+
if (!j)
|
|
88
|
+
return 'A applies a promotion from the journey view (J)';
|
|
89
|
+
refreshJourney(ui, true);
|
|
90
|
+
const p = j.pending.find((x) => x.to === 'always-on');
|
|
91
|
+
if (p) {
|
|
92
|
+
ui.confirm = { kind: 'promote', proposalId: p.proposalId };
|
|
93
|
+
return `promotion ${p.proposalId} → always-on (proposed by ${p.actor}): y applies, any key cancels`;
|
|
94
|
+
}
|
|
95
|
+
const real = j.pending.find((x) => x.to === 'real');
|
|
96
|
+
if (real)
|
|
97
|
+
return `real mode is armed only on your own terminal with your caps and a typed phrase: dexterm autotrade promote apply ${real.proposalId}`;
|
|
98
|
+
return j.view?.next?.stage === 'always-on' ? `nothing pending: ask the analyst to propose it, or in a terminal: dexterm autotrade promote ${j.id} --to always-on` : 'no promotion pending';
|
|
99
|
+
}
|
|
100
|
+
/** The y / any-key answer on the promotion card. */
|
|
101
|
+
export function confirmPromote(ui, proposalId, yes) {
|
|
102
|
+
if (!yes)
|
|
103
|
+
return 'promotion not applied (it stays pending)';
|
|
104
|
+
const j = ui.journey;
|
|
105
|
+
const got = j ? ui.deps.store.get(j.id) : null;
|
|
106
|
+
if (!got || !got.ok)
|
|
107
|
+
return 'the strategy is not valid any more';
|
|
108
|
+
const a = applyAlwaysOn(homeOf(ui), proposalId, got.strategy, { by: 'user:tui', now: ui.deps.now() });
|
|
109
|
+
refreshJourney(ui, true);
|
|
110
|
+
return a.ok ? `applied: ${got.strategy.id} is an always-on paper member (${a.memberId}); the service runs it from its next start` : `not applied: ${a.why}`;
|
|
111
|
+
}
|
|
112
|
+
export function journeyLines(j, now) {
|
|
113
|
+
if (j.error || !j.view)
|
|
114
|
+
return [{ text: j.error ?? 'loading…', tone: 'warn' }];
|
|
115
|
+
const v = j.view;
|
|
116
|
+
const out = [
|
|
117
|
+
{ text: `${v.id} v${v.version} · rules ${v.rulesHash.slice(0, 8)} · mode ${v.mode} · stage ${v.stage.toUpperCase()}`, tone: 'text' },
|
|
118
|
+
{ text: '', tone: 'dim' },
|
|
119
|
+
...v.rungs.map((r) => ({ text: `${r.ok ? '[x]' : '[ ]'} ${r.stage.padEnd(15)} ${r.detail}`, tone: (r.ok ? 'ok' : 'dim') })),
|
|
120
|
+
...v.notes.map((n) => ({ text: `! ${n}`, tone: 'warn' })),
|
|
121
|
+
{ text: v.next ? `next: ${v.next.stage} — ${v.next.how}` : 'top of the ladder: real (caps, kill switch, receipts: dexterm autotrade signal status)', tone: 'text' },
|
|
122
|
+
{ text: '', tone: 'dim' },
|
|
123
|
+
];
|
|
124
|
+
if (j.forward && j.report) {
|
|
125
|
+
const st = liveState(j.forward, now);
|
|
126
|
+
out.push({ text: `forward test ${j.forward.forwardId} · ${st.toUpperCase()} · ${windowText(j.forward.window)}`, tone: st === 'running' || st === 'starting' ? 'warn' : 'text' });
|
|
127
|
+
for (const l of j.report.lines.slice(1))
|
|
128
|
+
out.push({ text: l.replace(/^ /, ''), tone: 'text' });
|
|
129
|
+
out.push({ text: '', tone: 'dim' });
|
|
130
|
+
}
|
|
131
|
+
for (const p of j.pending)
|
|
132
|
+
out.push({ text: `pending promotion ${p.proposalId} → ${p.to} (proposed by ${p.actor})${p.to === 'always-on' ? ' · A to review' : ' · terminal only: dexterm autotrade promote apply'}`, tone: 'warn' });
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// strategy files; nothing here reaches a wallet, a key, a mandate or the signer.
|
|
5
5
|
import { INPUT_MAX, backtestSubject, beginFieldEdit, beginImport, beginNew, commitInput, commitTyping, confirmKey, exportSelected, nextView, openEditor, reloadList, refreshRunner, revalidateTyping, runnerVerb, saveEditor, selectedEntry, startBacktest, startCompareBacktest, startSweep, toggleMark, undoEditor, } from "./autotrade-ui.js";
|
|
6
6
|
import { openReview, reviewKey } from "./autotrade-proposals.js";
|
|
7
|
+
import { journeyForward, journeyPromote, journeyStopForward, openJourney, refreshJourney } from "./autotrade-journey.js";
|
|
7
8
|
const PRINTABLE = (k) => (k.name === 'space' ? ' ' : k.name === 'char' && k.ch && !k.ctrl && !k.alt && !/[\u0000-\u001f\u007f]/.test(k.ch) ? k.ch : null);
|
|
8
9
|
/** A captured key (input line, field value or confirm). Returns true when the key was consumed. */
|
|
9
10
|
export function autotradeKey(ui, key) {
|
|
@@ -135,6 +136,8 @@ export function autotradeAction(ui, action) {
|
|
|
135
136
|
case 'autotrade.save': return say(ui.view === 'editor' ? saveEditor(ui) : 's saves in the editor');
|
|
136
137
|
case 'autotrade.undo': return say(ui.view === 'editor' ? undoEditor(ui) : 'u undoes in the editor');
|
|
137
138
|
case 'autotrade.backtest': {
|
|
139
|
+
if (ui.view === 'journey' && ui.journey)
|
|
140
|
+
return say(startBacktest(ui, ui.journey.id));
|
|
138
141
|
if (ui.view === 'compare')
|
|
139
142
|
return say(startCompareBacktest(ui));
|
|
140
143
|
if (ui.view === 'editor' && ui.editor)
|
|
@@ -150,6 +153,16 @@ export function autotradeAction(ui, action) {
|
|
|
150
153
|
return say(id ? startSweep(ui, id) : 'no strategy selected');
|
|
151
154
|
}
|
|
152
155
|
case 'autotrade.proposal': return say(ui.view === 'list' ? openReview(ui) || null : 'o reviews chat proposals on the strategies list (esc)');
|
|
156
|
+
// task-1647: the journey view
|
|
157
|
+
case 'autotrade.journey':
|
|
158
|
+
if (ui.view === 'journey') {
|
|
159
|
+
refreshJourney(ui, true);
|
|
160
|
+
return say(null);
|
|
161
|
+
}
|
|
162
|
+
return say(openJourney(ui, ui.view === 'editor' ? ui.editor?.id : ui.view === 'backtest' ? backtestSubject(ui)?.id : e?.id));
|
|
163
|
+
case 'autotrade.forward': return say(ui.view === 'journey' ? journeyForward(ui) : 'F starts a forward test on the journey view (J)');
|
|
164
|
+
case 'autotrade.forwardstop': return say(ui.view === 'journey' ? journeyStopForward(ui) : 'X stops a forward test on the journey view (J)');
|
|
165
|
+
case 'autotrade.promote': return say(ui.view === 'journey' ? journeyPromote(ui) : 'A reviews a promotion on the journey view (J)');
|
|
153
166
|
case 'autotrade.run':
|
|
154
167
|
ui.view = 'runner';
|
|
155
168
|
refreshRunner(ui);
|
|
@@ -5,6 +5,7 @@ import { Canvas, header, footer, breakpoint, fmtAgeSec, sparkline, wrap } from '
|
|
|
5
5
|
import { compareRows as fleetCompareRows, compareTable as fleetCompareTable, fleetTotals } from '../../../autotrade/src/index.js';
|
|
6
6
|
import { tokenize } from "../screener/filter.js";
|
|
7
7
|
import { headerModel } from "../state.js";
|
|
8
|
+
import { journeyLines, refreshJourney } from "./autotrade-journey.js";
|
|
8
9
|
import { AUTOTRADE_VIEWS, VIEW_TITLE, READ_ONLY_PATHS, backtestSubject, compareRows, getAt, heartbeatStale, issuesByField, screenHighlight, } from "./autotrade-ui.js";
|
|
9
10
|
/** Per-screen memo of file reads + validation, keyed by what the list says about a strategy (id, hash, version,
|
|
10
11
|
* last backtest): a frame never re-reads a strategy file; a save, import or backtest reloads the list and so the key. */
|
|
@@ -46,10 +47,14 @@ export function renderAutotrade(c, state) {
|
|
|
46
47
|
renderCompare(c, ui, area);
|
|
47
48
|
else if (ui.view === 'backtest')
|
|
48
49
|
renderBacktest(c, ui, area, now);
|
|
50
|
+
else if (ui.view === 'journey')
|
|
51
|
+
renderJourneyView(c, ui, area, now);
|
|
49
52
|
else
|
|
50
53
|
renderRunner(c, ui, area, now);
|
|
51
|
-
if (ui.confirm)
|
|
54
|
+
if (ui.confirm?.kind === 'flatten')
|
|
52
55
|
confirmCard(c);
|
|
56
|
+
else if (ui.confirm?.kind === 'promote')
|
|
57
|
+
promoteCard(c, ui, ui.confirm.proposalId);
|
|
53
58
|
if (ui.review)
|
|
54
59
|
reviewCard(c, ui);
|
|
55
60
|
footer(c, hintsFor(ui), t.name);
|
|
@@ -68,6 +73,8 @@ function tabs(c, ui) {
|
|
|
68
73
|
const off = (v === 'compare' && !ui.compare) || (v === 'editor' && !ui.editor);
|
|
69
74
|
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
75
|
}
|
|
76
|
+
if (ui.view === 'journey')
|
|
77
|
+
x = c.text(x, y, ` ${VIEW_TITLE.journey} `, { fg: t.text, bg: t.tabActiveBg, b: true }) + 1;
|
|
71
78
|
const line = ui.input
|
|
72
79
|
? `${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
80
|
: ui.notice;
|
|
@@ -77,7 +84,7 @@ function tabs(c, ui) {
|
|
|
77
84
|
function hintsFor(ui) {
|
|
78
85
|
const common = [['esc', 'back'], ['q', 'quit']];
|
|
79
86
|
if (ui.confirm)
|
|
80
|
-
return [['y', 'flatten'], ['any key', 'cancel']];
|
|
87
|
+
return [['y', ui.confirm.kind === 'promote' ? 'apply promotion' : 'flatten'], ['any key', 'cancel']];
|
|
81
88
|
if (ui.review)
|
|
82
89
|
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
90
|
if (ui.input)
|
|
@@ -89,6 +96,7 @@ function hintsFor(ui) {
|
|
|
89
96
|
: [['tab', 'view'], ['↑↓', 'field'], ['⏎', 'change'], ['s', 'save'], ['u', 'undo'], ['b', 'backtest'], ['w', 'sweep'], ...common];
|
|
90
97
|
case 'compare': return [['tab', 'view'], ['↑↓', 'scroll'], ['b', 'backtest both, one tape'], ...common];
|
|
91
98
|
case 'backtest': return [['tab', 'view'], ['b', 'run again'], ['w', 'sweep (held-out)'], ...common];
|
|
99
|
+
case 'journey': return [['b', 'backtest'], ['F', 'forward test'], ['X', 'stop it'], ['A', 'promotion'], ['J', 'refresh'], ...common];
|
|
92
100
|
default: return [['tab', 'view'], ['!', 'kill'], ['p', 'pause entries'], ['f', 'flatten (confirm)'], ...common];
|
|
93
101
|
}
|
|
94
102
|
}
|
|
@@ -206,7 +214,7 @@ function renderList(c, ui, a, now) {
|
|
|
206
214
|
}
|
|
207
215
|
const running = (ui) => ui.runner.kind === 'ok';
|
|
208
216
|
/** 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'; };
|
|
217
|
+
const runMode = (ui) => { const m = ui.runner.kind === 'ok' ? ui.runner.snap.mode : 'paper'; return m === 'live' || m === 'devnet' || m === 'real' ? m : 'paper'; };
|
|
210
218
|
/** One chat proposal card (§2a): id, hashes, the diff lines, validation, LOOSER in the warn colour. */
|
|
211
219
|
export function proposalCard(c, p, x, y, w, maxY) {
|
|
212
220
|
const t = c.theme;
|
|
@@ -639,9 +647,9 @@ function sweepRow(x, pw) {
|
|
|
639
647
|
function renderRunner(c, ui, a, now) {
|
|
640
648
|
const t = c.theme;
|
|
641
649
|
const rr = ui.runner;
|
|
642
|
-
const sh = 5;
|
|
643
650
|
const mode = runMode(ui);
|
|
644
|
-
const
|
|
651
|
+
const sh = mode === 'real' ? (ui.runner.kind === 'ok' && ui.runner.snap.real?.limits ? 8 : 6) : 5;
|
|
652
|
+
const sbox = c.box(a.x, a.y, a.w, sh, { title: `RUNNER · ${mode === 'live' ? 'LIVE · MAINNET' : mode === 'devnet' ? 'DEVNET (live signer rehearsal)' : mode === 'real' ? 'REAL · LIVE TRADE FIRE signals to your executor (no key here)' : 'PAPER'}`, caption: `status file: ${rr.path || '~/.dexterm/autotrade/runner.json'}`, focus: true });
|
|
645
653
|
let y = sbox.y;
|
|
646
654
|
const sl = (s, fg = t.text, b = false) => { if (y < sbox.y + sbox.h) {
|
|
647
655
|
c.text(sbox.x + 1, y, fit(s, sbox.w - 2), { fg, bg: sbox.bg, b });
|
|
@@ -665,6 +673,23 @@ function renderRunner(c, ui, a, now) {
|
|
|
665
673
|
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
674
|
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
675
|
sl(s.haltReason ? `halted: ${s.haltReason}` : 'no halt', s.haltReason ? t.down : t.dim, !!s.haltReason);
|
|
676
|
+
// task-1645: real mode: the account, the kill switch, the outbox, each channel's health, unconfirmed positions.
|
|
677
|
+
const rm = s.real;
|
|
678
|
+
if (rm) {
|
|
679
|
+
const chans = rm.channels.map((h) => `${h.name} ${h.failing ? `FAILING ×${h.failing}` : 'ok'}${h.pending ? ` (${h.pending} pending)` : ''}`).join(', ') || 'none';
|
|
680
|
+
const unconf = rm.positions.filter((p) => p.state === 'unconfirmed').length;
|
|
681
|
+
sl(`real · account ${rm.accountId} · ${rm.killed ? `KILL ON (${rm.killed})` : 'kill off'} · outbox ${rm.outboxDepth} pending · ${chans} · ${unconf} unconfirmed · today ${rm.day.buySol} of ${rm.caps.dailySol ?? 'off'} SOL`, rm.killed || rm.channels.some((h) => h.failing) || unconf ? t.warn : t.text, true);
|
|
682
|
+
// task-1654: the limits panel (status only: limits change on your own terminal, `dexterm autotrade limits`).
|
|
683
|
+
if (rm.limits) {
|
|
684
|
+
const L = rm.limits;
|
|
685
|
+
const v = (k, unit) => (typeof L[k] === 'number' ? `${L[k]}${unit}` : 'off');
|
|
686
|
+
const pause = rm.risk?.buyPause;
|
|
687
|
+
sl(`limits · ${v('perTradeSol', ' SOL')}/trade${typeof L.perTradePct === 'number' ? ` (${L.perTradePct}% of book)` : ''} · ${v('dailySol', ' SOL')}/day · ${v('buysPerDay', '')} buys · ${v('maxOpenPositions', '')} open · loss stop ${v('dailyLossSol', ' SOL')} · streak ${rm.risk?.lossStreak ?? 0}/${v('lossStreak', '')}${rm.risk?.breaker ? ' · BREAKER TRIPPED' : ''}${pause ? ` · BUYS PAUSED: ${pause}` : ''} · dexterm autotrade limits show`, pause || rm.risk?.breaker ? t.warn : t.muted, !!pause);
|
|
688
|
+
const ex = rm.executor;
|
|
689
|
+
if (ex)
|
|
690
|
+
sl(`executor · ${ex.alive === false ? 'SILENT' : ex.alive ? 'alive' : 'not judged'}${ex.silentSec !== null && ex.lastSeenBy !== 'run-start' ? ` · last ${ex.lastSeenBy} ${fmtAgeSec(ex.silentSec)} ago` : ''}${ex.heartbeat ? ` · ${ex.heartbeat.executor}${ex.heartbeat.dryRun ? ' (dry run)' : ''}` : ' · no heartbeat yet'}${ex.lastReceipt ? ` · receipt #${ex.lastReceipt.nonce} ${ex.lastReceipt.status}` : ''}${ex.receiptLagSec !== null ? ` · lag ${ex.receiptLagSec} s` : ''}${ex.windowMin !== null ? ` · pauses buys after ${ex.windowMin} min silent` : ''}`, ex.alive === false ? t.down : t.muted, ex.alive === false);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
668
693
|
// task-1529: a `run --all` shows its strategies side by side above the shared panels (rows tagged [strategy]).
|
|
669
694
|
const fleet = ui.fleet;
|
|
670
695
|
let fh = 0;
|
|
@@ -703,7 +728,7 @@ function renderRunner(c, ui, a, now) {
|
|
|
703
728
|
[` last: ${k.lastDecision ?? '—'}`, t.muted],
|
|
704
729
|
]));
|
|
705
730
|
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],
|
|
731
|
+
[`${tag(p)}${p.symbol.padEnd(8)}${pct(p.pnlPct).padStart(8)}${usd(p.pnlUsd).padStart(9)}${p.freeBag ? ' free bag' : ''}${p.confirm ? ` · ${p.confirm}` : ''}`, p.pnlUsd >= 0 ? t.up : t.down],
|
|
707
732
|
[` ${p.profile} · next: ${p.next ?? '—'}`, t.muted],
|
|
708
733
|
]));
|
|
709
734
|
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' });
|
|
@@ -723,3 +748,29 @@ function confirmCard(c) {
|
|
|
723
748
|
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
749
|
c.text(b.x + 1, b.y + 3, 'y flatten · any other key cancels', { fg: t.warn, bg: b.bg, b: true });
|
|
725
750
|
}
|
|
751
|
+
// ───────────────────────────── journey (task-1647) ─────────────────────────────
|
|
752
|
+
function renderJourneyView(c, ui, a, now) {
|
|
753
|
+
const t = c.theme;
|
|
754
|
+
refreshJourney(ui);
|
|
755
|
+
const j = ui.journey;
|
|
756
|
+
const box = c.box(a.x, a.y, a.w, a.h, { title: `JOURNEY · ${j?.id ?? '—'}`, caption: 'draft → backtested → forward-tested → always-on (paper) → real · one rung at a time, on these exact rules', focus: true });
|
|
757
|
+
const bg = box.bg;
|
|
758
|
+
if (!j) {
|
|
759
|
+
c.text(box.x + 1, box.y, 'J on a strategy opens its journey', { fg: t.dim, bg });
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const tone = { text: t.text, ok: t.up, warn: t.warn, dim: t.dim };
|
|
763
|
+
// tables keep their columns: only a line wider than the box wraps
|
|
764
|
+
const lines = journeyLines(j, now).flatMap((l) => (l.text.length <= box.w - 2 ? [l] : wrap(l.text, box.w - 2).map((x) => ({ text: x, tone: l.tone }))));
|
|
765
|
+
lines.slice(0, box.h).forEach((l, i) => c.text(box.x + 1, box.y + i, fit(l.text, box.w - 2), { fg: tone[l.tone], bg }));
|
|
766
|
+
}
|
|
767
|
+
function promoteCard(c, ui, proposalId) {
|
|
768
|
+
const t = c.theme;
|
|
769
|
+
const p = ui.journey?.pending.find((x) => x.proposalId === proposalId);
|
|
770
|
+
const body = p ? p.card : [`promotion ${proposalId}`];
|
|
771
|
+
const w = Math.min(c.w - 4, 104), h = Math.min(c.h - 4, body.length + 5);
|
|
772
|
+
const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
|
|
773
|
+
const box = c.box(x, y, w, h, { title: 'APPLY PROMOTION? (paper: the always-on service)', focus: true });
|
|
774
|
+
body.slice(0, h - 4).forEach((l, i) => c.text(box.x + 1, box.y + i, fit(l, box.w - 2), { fg: t.text, bg: box.bg }));
|
|
775
|
+
c.text(box.x + 1, box.y + h - 3, fit(`proposed by ${p?.actor ?? '?'} · y applies (autotrade.json gets the paper member) · any other key cancels`, box.w - 2), { fg: t.warn, bg: box.bg, b: true });
|
|
776
|
+
}
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
// cards; applying them is the chat card's keypress flow). Kill / pause / flatten go through RunnerControl: the
|
|
16
16
|
// runner's control file (task-1509 fileControl), the same request `dexterm autotrade stop|flatten` writes.
|
|
17
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
19
|
import { applyOps, dextermHome } from '../../../custom/src/index.js';
|
|
20
|
-
import { StrategyStore, MAX_OPS, STRATEGY_ID, activeRunner, bumpPatch, fmtIssue, readControl, readFleetStatus, requestControl, runnerRendezvousPath, validateStrategy, } from '../../../autotrade/src/index.js';
|
|
20
|
+
import { StrategyStore, MAX_OPS, recordBacktestEvidence, STRATEGY_ID, activeRunner, bumpPatch, fmtIssue, readControl, readFleetStatus, requestControl, runnerRendezvousPath, validateStrategy, } from '../../../autotrade/src/index.js';
|
|
21
21
|
import { parseFilter } from "../screener/filter.js";
|
|
22
22
|
import { proposalFilesFor, proposalSource } from "./autotrade-proposals.js";
|
|
23
|
+
import { confirmPromote } from "./autotrade-journey.js";
|
|
23
24
|
export const AUTOTRADE_VIEWS = ['list', 'editor', 'compare', 'backtest', 'runner'];
|
|
24
|
-
export const VIEW_TITLE = { list: 'STRATEGIES', editor: 'EDITOR', compare: 'COMPARE', backtest: 'BACKTEST', runner: 'RUNNER' };
|
|
25
|
+
export const VIEW_TITLE = { list: 'STRATEGIES', editor: 'EDITOR', compare: 'COMPARE', backtest: 'BACKTEST', runner: 'RUNNER', journey: 'JOURNEY' };
|
|
25
26
|
export const SECTIONS = ['entry', 'exits', 'risk', 'sizing', 'agent', 'execution'];
|
|
26
27
|
/** Who the journal names for an edit made on this screen (the same actor the CLI uses for the user's own edits). */
|
|
27
28
|
export const TUI_ACTOR = 'user';
|
|
@@ -94,9 +95,9 @@ function runnerShapeError(d) {
|
|
|
94
95
|
const o = d;
|
|
95
96
|
if (o.schema !== RUNNER_SCHEMA)
|
|
96
97
|
return `unknown rendezvous shape ${JSON.stringify(o.schema)} (expected ${RUNNER_SCHEMA})`;
|
|
97
|
-
// Paper
|
|
98
|
-
if (o.mode !== 'paper')
|
|
99
|
-
return `mode ${JSON.stringify(o.mode)}: this screen shows paper runners only`;
|
|
98
|
+
// Paper, or real (task-1645: signals to the user's executor). Anything else is an error, never a running live runner.
|
|
99
|
+
if (o.mode !== 'paper' && o.mode !== 'real')
|
|
100
|
+
return `mode ${JSON.stringify(o.mode)}: this screen shows paper runners only (and real-mode signal runners)`;
|
|
100
101
|
for (const k of ['pid', 'startedAt', 'heartbeatAt'])
|
|
101
102
|
if (typeof o[k] !== 'number')
|
|
102
103
|
return `${k} is not a number`;
|
|
@@ -133,7 +134,7 @@ export function defaultDeps(env = process.env, jobs = null) {
|
|
|
133
134
|
export function newAutotradeUi(deps) {
|
|
134
135
|
const ui = {
|
|
135
136
|
view: 'list', deps, entries: [], selected: 0, marked: [], notice: null, input: null, editor: null, compare: null,
|
|
136
|
-
focusId: null, backtests: new Map(), compareRun: null, sweep: null, runner: { kind: 'none', path: '' }, fleet: null, runnerReadAt: 0, confirm: null, review: null,
|
|
137
|
+
focusId: null, backtests: new Map(), compareRun: null, sweep: null, runner: { kind: 'none', path: '' }, fleet: null, runnerReadAt: 0, confirm: null, review: null, journey: null,
|
|
137
138
|
};
|
|
138
139
|
reloadList(ui);
|
|
139
140
|
refreshRunner(ui);
|
|
@@ -525,6 +526,13 @@ export function startBacktest(ui, id) {
|
|
|
525
526
|
job.result = out;
|
|
526
527
|
job.doneAt = ui.deps.now();
|
|
527
528
|
recordLine(ui, s, out.report);
|
|
529
|
+
// task-1647: the held-out split of a stored strategy is its journey evidence
|
|
530
|
+
if (out.evidence && ui.deps.store.get(s.id)?.ok) {
|
|
531
|
+
try {
|
|
532
|
+
recordBacktestEvidence(dirname(ui.deps.store.dir), out.evidence);
|
|
533
|
+
}
|
|
534
|
+
catch { /* evidence is best effort here */ }
|
|
535
|
+
}
|
|
528
536
|
}, (e) => { job.status = 'error'; job.error = errText(e); job.doneAt = ui.deps.now(); });
|
|
529
537
|
return `backtesting ${s.id} v${s.version} on the last ${BACKTEST_DAYS} days of recorded history…`;
|
|
530
538
|
}
|
|
@@ -631,6 +639,8 @@ export function confirmKey(ui, yes) {
|
|
|
631
639
|
ui.confirm = null;
|
|
632
640
|
if (!c)
|
|
633
641
|
return '';
|
|
642
|
+
if (c.kind === 'promote')
|
|
643
|
+
return confirmPromote(ui, c.proposalId, yes);
|
|
634
644
|
return yes ? ui.deps.control.flatten() : 'flatten cancelled';
|
|
635
645
|
}
|
|
636
646
|
/** The views tab cycles through (compare only once two strategies were marked). */
|
|
@@ -7,7 +7,7 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
|
|
8
8
|
import { defaultDbPath } from '../../../data/src/index.js';
|
|
9
9
|
import { openHistoryReadOnly } from '../../../recorder/src/index.js';
|
|
10
|
-
import { backtest, sweep, tapeFromDb, tapeFromDoc } from '../../../autotrade/src/index.js';
|
|
10
|
+
import { backtest, backtestEvidence, sweep, tapeFromDb, tapeFromDoc } from '../../../autotrade/src/index.js';
|
|
11
11
|
const DAY_MS = 86_400_000;
|
|
12
12
|
/** The equity curve is resampled to this many points before it crosses the thread boundary (it feeds a sparkline). */
|
|
13
13
|
const CURVE_POINTS = 240;
|
|
@@ -42,7 +42,9 @@ export async function backtestOn(tape, ss) {
|
|
|
42
42
|
const out = [];
|
|
43
43
|
for (const s of ss) {
|
|
44
44
|
const { report, run } = await backtest(s, tape);
|
|
45
|
-
|
|
45
|
+
// task-1647: one strategy alone (not a compare) also runs on each half of the held-out split: journey evidence
|
|
46
|
+
const evidence = ss.length === 1 && s.mode === 'paper' ? (await backtestEvidence(s, tape, { by: 'user:tui', full: { report, run } })).evidence : undefined;
|
|
47
|
+
out.push({ report, curve: resample(run.equityCurve.map((p) => p.equity), CURVE_POINTS), ...(evidence ? { evidence } : {}) });
|
|
46
48
|
}
|
|
47
49
|
return out;
|
|
48
50
|
}
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
// new launches / runners / pace / "what is working" score. No model in the hot path; the board carries
|
|
4
4
|
// its own limits (name-based, X/TG-only narratives missed, honest unclassified share). w flips 1h/24h,
|
|
5
5
|
// M opens the HUD popup. The board is computed in state.ts (computeNarrativeView) on entry and the tick.
|
|
6
|
+
// task-1818: `n` flips to the EXPERIMENTAL heat table (@dexterm/narratives: clustered by what the name says,
|
|
7
|
+
// lifecycle + traction score per narrative); ⏎ drills into one narrative's member coins, esc returns to the table.
|
|
6
8
|
import { Canvas, header, footer, badge, breakpoint } from '../../../tui/src/index.js';
|
|
7
9
|
import { headerModel } from "../state.js";
|
|
10
|
+
import { clean } from '../../../packs/src/index.js';
|
|
11
|
+
import { HEAT_NOTE } from '../../../narratives/src/index.js';
|
|
12
|
+
import { heatRows, drillDetail, currentTracker, narrativesSource, UNTRUSTED_NOTE } from "../narratives.js";
|
|
8
13
|
const fmtUsd = (v) => { if (v == null)
|
|
9
14
|
return '—'; const a = Math.abs(v); if (a >= 1e9)
|
|
10
15
|
return (v / 1e9).toFixed(2) + 'B'; if (a >= 1e6)
|
|
@@ -12,6 +17,13 @@ const fmtUsd = (v) => { if (v == null)
|
|
|
12
17
|
return (v / 1e3).toFixed(1) + 'K'; return v.toFixed(0); };
|
|
13
18
|
const fmtPct = (v) => (v == null ? '—' : (v > 0 ? '+' : '') + v.toFixed(1) + '%');
|
|
14
19
|
export function renderNarrative(c, state) {
|
|
20
|
+
if (state.narrativesUi?.mode === 'heat') {
|
|
21
|
+
if (state.narrativesUi.drill)
|
|
22
|
+
renderDrill(c, state);
|
|
23
|
+
else
|
|
24
|
+
renderHeat(c, state);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
15
27
|
const t = c.theme;
|
|
16
28
|
const bp = breakpoint(c.w);
|
|
17
29
|
header(c, headerModel(state));
|
|
@@ -27,7 +39,7 @@ export function renderNarrative(c, state) {
|
|
|
27
39
|
if (!board || board.narratives.every((n) => n.tokens === 0)) {
|
|
28
40
|
c.textC(box.x, box.y + 1, box.w, 'collecting narratives — run the app with the recorder on, or open the screener first', { fg: t.muted, bg, i: true });
|
|
29
41
|
c.textC(box.x, box.y + 3, box.w, view?.error ? `(${view.error})` : 'clusters come from token names against a lexicon (owner: maintainers); X/Telegram-only narratives are missed', { fg: t.dim, bg });
|
|
30
|
-
footer(c, [['w', '1h/24h'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
42
|
+
footer(c, [['w', '1h/24h'], ['n', 'narratives (exp)'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
31
43
|
return;
|
|
32
44
|
}
|
|
33
45
|
const shown = board.narratives.filter((n) => n.tokens > 0);
|
|
@@ -86,5 +98,145 @@ export function renderNarrative(c, state) {
|
|
|
86
98
|
const ly = box.y + box.h - 1;
|
|
87
99
|
if (wide)
|
|
88
100
|
c.textC(box.x, ly, box.w, `model output: none (deterministic lexicon v${board.meta.lexicon_version}) · ${board.meta.copycat_tokens} copycat symbols · name-based · pace '*' = all tokens · compute ${board.meta.compute_ms}ms`, { fg: t.dim, bg });
|
|
89
|
-
footer(c, [['↑↓', 'move'], ['w', win === '1h' ? '→24h' : '→1h'], ['M', 'popup'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
101
|
+
footer(c, [['↑↓', 'move'], ['w', win === '1h' ? '→24h' : '→1h'], ['n', 'narratives (exp)'], ['M', 'popup'], ['esc', 'back'], ['q', 'quit']], t.name);
|
|
102
|
+
}
|
|
103
|
+
// ---- task-1818 EXPERIMENTAL heat table + drill-down ------------------------------------------------------------
|
|
104
|
+
const heatUsd = (v) => (v == null ? '—' : '$' + fmtUsd(v));
|
|
105
|
+
const fmtShare = (v) => (v == null ? '—' : (v * 100).toFixed(1) + '%');
|
|
106
|
+
const fmtX = (v) => (v == null || !Number.isFinite(v) ? '—' : v.toFixed(2) + 'x');
|
|
107
|
+
function lcColor(t, lc) {
|
|
108
|
+
// states are measured heat, so none is painted as a market up/down colour (no green "go")
|
|
109
|
+
return lc === 'ignition' ? t.accent : lc === 'peak' ? t.text : lc === 'fading' ? t.warn : lc === 'dead' ? t.dim : t.muted;
|
|
110
|
+
}
|
|
111
|
+
/** The limits line: the full HEAT_NOTE when it fits, else the short form (the note must never be the part that is cut). */
|
|
112
|
+
function limitsLine(w, extra) {
|
|
113
|
+
const full = `${HEAT_NOTE} · ${UNTRUSTED_NOTE} · ${extra}`;
|
|
114
|
+
return full.length <= w ? full : `measured heat, not advice · ${UNTRUSTED_NOTE} · ${extra}`;
|
|
115
|
+
}
|
|
116
|
+
/** The status line under an empty heat table: building (with the one-time stall warning), failed, or nothing yet. */
|
|
117
|
+
function heatPlaceholder(state) {
|
|
118
|
+
const ui = state.narrativesUi;
|
|
119
|
+
const rec = narrativesSource(state) === 'recorded';
|
|
120
|
+
if (ui?.error)
|
|
121
|
+
return [`narratives failed: ${clean(ui.error, 120)}`, 'the theme board (v) still works'];
|
|
122
|
+
if (!currentTracker(state))
|
|
123
|
+
return [rec ? 'building narratives from recorded history…' : 'building narratives from the visible rows…',
|
|
124
|
+
rec ? 'first build reads the last 26 h of the recorder: tens of seconds, the screen stays live (once per run)' : 'no recorder DB here (fixtures / replay): no history, so every narrative starts as a spark'];
|
|
125
|
+
return ['no narrative with 2+ coins yet', 'a narrative needs at least two coins whose names share a theme · measured heat, not advice'];
|
|
126
|
+
}
|
|
127
|
+
function renderHeat(c, state) {
|
|
128
|
+
const t = c.theme;
|
|
129
|
+
const bp = breakpoint(c.w);
|
|
130
|
+
header(c, headerModel(state));
|
|
131
|
+
const ui = state.narrativesUi;
|
|
132
|
+
const rows = heatRows(state);
|
|
133
|
+
const tr = currentTracker(state);
|
|
134
|
+
const src = narrativesSource(state) === 'recorded' ? 'recorded history' : 'visible rows';
|
|
135
|
+
const top = 2, bottom = c.h - 1;
|
|
136
|
+
const title = `NARRATIVES · EXPERIMENTAL · ${tr ? `${rows.length} narratives · ${tr.coins.size} coins` : 'building'} · ${src}${ui.busy && tr ? ' · refreshing' : ''}`;
|
|
137
|
+
const caption = bp === 'compact' ? undefined : 'measured heat, not advice';
|
|
138
|
+
const box = c.box(0, top, c.w, bottom - top, { title, caption, focus: true });
|
|
139
|
+
const bg = box.bg;
|
|
140
|
+
const keys = [['↑↓', 'move'], ['⏎', 'drill'], ['n', 'theme board'], ['esc', 'back'], ['q', 'quit']];
|
|
141
|
+
if (!rows.length) {
|
|
142
|
+
const [a, b] = heatPlaceholder(state);
|
|
143
|
+
c.textC(box.x, box.y + 1, box.w, a, { fg: t.muted, bg, i: true });
|
|
144
|
+
c.textC(box.x, box.y + 3, box.w, b, { fg: t.dim, bg });
|
|
145
|
+
c.textC(box.x, box.y + 5, box.w, HEAT_NOTE, { fg: t.dim, bg });
|
|
146
|
+
footer(c, keys, t.name);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sel = Math.max(0, Math.min(rows.length - 1, ui.selected));
|
|
150
|
+
// columns: [x, header]; a column that does not fit the box is dropped (the runner first)
|
|
151
|
+
const cols = [[1, '#', 3], [5, 'NARRATIVE', 18], [24, 'THEME', 12], [37, 'STATE', 10], [48, 'COINS', 5], [54, 'NEW/h', 5], [60, 'VOL 1h', 9], [70, 'SHARE', 6], [77, 'TRADERS', 7], [85, 'SCORE', 5], [91, 'TOP RUNNER (x · drawdown)', 28]];
|
|
152
|
+
const fits = (i) => cols[i][0] + Math.min(cols[i][2], 12) <= box.w - 1;
|
|
153
|
+
let cy = box.y;
|
|
154
|
+
cols.forEach(([x, h], i) => { if (fits(i))
|
|
155
|
+
c.text(box.x + x, cy, h, { fg: t.dim, bg }, box.w - x - 1); });
|
|
156
|
+
cy += 1;
|
|
157
|
+
c.hline(box.x, cy, box.w, { fg: t.border, bg });
|
|
158
|
+
cy += 1;
|
|
159
|
+
const scroll = Math.max(0, sel - (box.h - 6));
|
|
160
|
+
for (let i = scroll; i < rows.length; i++) {
|
|
161
|
+
if (cy >= box.y + box.h - 2)
|
|
162
|
+
break;
|
|
163
|
+
const s = rows[i];
|
|
164
|
+
const w1 = s.windows['1h'];
|
|
165
|
+
const selected = i === sel;
|
|
166
|
+
const rb = selected ? t.selBg : bg;
|
|
167
|
+
c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
|
|
168
|
+
if (selected)
|
|
169
|
+
c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
|
|
170
|
+
const cell = (k, v, st) => { if (fits(k))
|
|
171
|
+
c.text(box.x + cols[k][0], cy, v, { ...st, bg: rb }, Math.min(cols[k][2], box.w - cols[k][0] - 1)); };
|
|
172
|
+
cell(0, s.rank == null ? '·' : String(s.rank), { fg: s.rank == null ? t.dim : t.accent, b: true });
|
|
173
|
+
cell(1, clean(s.label, 18), { fg: selected ? t.borderFocus : t.text, b: s.rank != null });
|
|
174
|
+
cell(2, s.theme ? clean(s.theme, 12) : '—', { fg: t.muted });
|
|
175
|
+
cell(3, s.lifecycle + (s.saturated ? '*' : ''), { fg: lcColor(t, s.lifecycle), b: s.lifecycle === 'ignition' });
|
|
176
|
+
cell(4, String(s.members), { fg: t.text });
|
|
177
|
+
cell(5, w1.newPerHour.toFixed(0), { fg: t.muted });
|
|
178
|
+
cell(6, heatUsd(w1.volume), { fg: t.text });
|
|
179
|
+
cell(7, fmtShare(w1.volumeShare), { fg: t.muted });
|
|
180
|
+
cell(8, String(w1.traders), { fg: t.muted });
|
|
181
|
+
cell(9, String(s.score), { fg: t.accent, b: true });
|
|
182
|
+
const r = s.topRunner;
|
|
183
|
+
cell(10, r ? `${clean(r.symbol, 10)} ${fmtX(r.x)} · ${r.drawdownPct.toFixed(0)}%` : '—', { fg: r ? t.text : t.dim });
|
|
184
|
+
cy += 1;
|
|
185
|
+
}
|
|
186
|
+
c.textC(box.x, box.y + box.h - 1, box.w, limitsLine(box.w, '* = copy-cat saturated'), { fg: t.dim, bg });
|
|
187
|
+
footer(c, keys, t.name);
|
|
188
|
+
}
|
|
189
|
+
function renderDrill(c, state) {
|
|
190
|
+
const t = c.theme;
|
|
191
|
+
header(c, headerModel(state));
|
|
192
|
+
const ui = state.narrativesUi;
|
|
193
|
+
const d = drillDetail(state);
|
|
194
|
+
const top = 2, bottom = c.h - 1;
|
|
195
|
+
const keys = [['↑↓', 'move'], ['⏎', 'open coin'], ['esc', 'table'], ['q', 'quit']];
|
|
196
|
+
if (!d) {
|
|
197
|
+
const box = c.box(0, top, c.w, bottom - top, { title: 'NARRATIVE · EXPERIMENTAL', focus: true });
|
|
198
|
+
c.textC(box.x, box.y + 1, box.w, 'this narrative is no longer tracked (esc returns to the table)', { fg: t.muted, bg: box.bg, i: true });
|
|
199
|
+
footer(c, keys, t.name);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const w1 = d.windows['1h'];
|
|
203
|
+
const title = `NARRATIVE · ${clean(d.label, 24)} · ${d.lifecycle}${d.saturated ? ' · copy-cat saturated' : ''} · ${d.memberList.length} coin${d.memberList.length === 1 ? '' : 's'} · EXPERIMENTAL`;
|
|
204
|
+
const box = c.box(0, top, c.w, bottom - top, { title, caption: 'measured heat, not advice · members in spawn order', focus: true });
|
|
205
|
+
const bg = box.bg;
|
|
206
|
+
let cy = box.y;
|
|
207
|
+
c.text(box.x + 1, cy, `lifecycle rule (${d.lifecycle}): ${d.lifecycleRule}`, { fg: lcColor(t, d.lifecycle), bg }, box.w - 2);
|
|
208
|
+
cy += 1;
|
|
209
|
+
c.text(box.x + 1, cy, `1h: vol ${heatUsd(w1.volume)} · share ${fmtShare(w1.volumeShare)} · new/h ${w1.newPerHour.toFixed(0)} · traders ${w1.traders} · score ${d.score}${d.rank ? ` · rank ${d.rank}` : ''}${d.theme ? ` · theme ${clean(d.theme, 16)}` : ''}`, { fg: t.muted, bg }, box.w - 2);
|
|
210
|
+
cy += 1;
|
|
211
|
+
const cols = [[1, '#', 4], [6, 'SYMBOL', 12], [19, 'NAME', 26], [46, 'X SINCE 1ST', 11], [58, 'LIQ', 9], [68, 'VOL 1h', 9]];
|
|
212
|
+
const fits = (i) => cols[i][0] + Math.min(cols[i][2], 8) <= box.w - 1;
|
|
213
|
+
cols.forEach(([x, h], i) => { if (fits(i))
|
|
214
|
+
c.text(box.x + x, cy, h, { fg: t.dim, bg }); });
|
|
215
|
+
cy += 1;
|
|
216
|
+
c.hline(box.x, cy, box.w, { fg: t.border, bg });
|
|
217
|
+
cy += 1;
|
|
218
|
+
const n = d.memberList.length;
|
|
219
|
+
const sel = Math.max(0, Math.min(n - 1, ui.drill.selected));
|
|
220
|
+
const scroll = Math.max(0, sel - (box.h - 8));
|
|
221
|
+
for (let i = scroll; i < n; i++) {
|
|
222
|
+
if (cy >= box.y + box.h - 1)
|
|
223
|
+
break;
|
|
224
|
+
const m = d.memberList[i];
|
|
225
|
+
const selected = i === sel;
|
|
226
|
+
const rb = selected ? t.selBg : bg;
|
|
227
|
+
c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
|
|
228
|
+
if (selected)
|
|
229
|
+
c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
|
|
230
|
+
const cell = (k, v, fg) => { if (fits(k))
|
|
231
|
+
c.text(box.x + cols[k][0], cy, v, { fg, bg: rb }, Math.min(cols[k][2], box.w - cols[k][0] - 1)); };
|
|
232
|
+
cell(0, `#${m.spawn}`, t.dim);
|
|
233
|
+
cell(1, clean(m.symbol, 12), selected ? t.borderFocus : t.text);
|
|
234
|
+
cell(2, clean(m.name, 26), t.muted);
|
|
235
|
+
cell(3, fmtX(m.x), m.x == null ? t.dim : m.x >= 1 ? t.up : t.down);
|
|
236
|
+
cell(4, heatUsd(m.liq), t.text);
|
|
237
|
+
cell(5, heatUsd(m.vol1h), t.text);
|
|
238
|
+
cy += 1;
|
|
239
|
+
}
|
|
240
|
+
c.textC(box.x, box.y + box.h - 1, box.w, limitsLine(box.w, 'x = now vs first $10K+ pool price'), { fg: t.dim, bg });
|
|
241
|
+
footer(c, keys, t.name);
|
|
90
242
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// The runner starts the bot by itself when it is set up and paired (`dexterm autotrade run … --paper`; --no-telegram
|
|
16
16
|
// keeps it off). Nothing here is hosted: the bot long-polls from this machine.
|
|
17
17
|
import { dextermHome } from '../../custom/src/index.js';
|
|
18
|
-
import { ChatAudit, PairingWindow, TelegramConnector, TIERS_ALL, chooseSecretStore, connectorsPath, defaultTelegram, loadConnectors, looksLikeBotToken, redact, saveConnectors, secretStore, verifyChatAudit, ChatHub, } from '../../autotrade/src/index.js';
|
|
18
|
+
import { ChatAudit, PairingWindow, TelegramConnector, TIERS_ALL, chooseSecretStore, connectorsPath, defaultTelegram, loadConnectors, looksLikeBotToken, redact, saveConnectors, secretStore, verifyChatAudit, ChatHub, enabledChannels, parseSignalsConfig, } from '../../autotrade/src/index.js';
|
|
19
19
|
export const TELEGRAM_USAGE = `dexterm telegram: your own Telegram bot for the autotrade runner (paper first). Nothing is hosted.
|
|
20
20
|
|
|
21
21
|
dexterm telegram setup [--tiers notify,control,approve] [--no-pair]
|
|
@@ -321,6 +321,8 @@ export async function telegramCommand(argv, io, deps = {}) {
|
|
|
321
321
|
paired: t.allow.map((a) => ({ senderId: a.senderId, chatId: a.chatId, tiers: a.tiers, pairedAt: a.pairedAt ? new Date(a.pairedAt).toISOString() : null })),
|
|
322
322
|
approvals: t.approvals, approvalExpirySec: t.approvalExpirySec, repriceTolPct: t.repriceTolPct, notify: t.notify, dailySummaryAt: t.dailySummaryAt, quietHours: t.quietHours,
|
|
323
323
|
audit: { days: chain.length, broken: chain.filter((c) => !c.check.ok).map((c) => c.day) },
|
|
324
|
+
// task-1645: the real-mode trade-fire channels live in the same file (dexterm autotrade signal channels).
|
|
325
|
+
signals: (() => { const sc = parseSignalsConfig(cfg.signals); return { channels: enabledChannels(sc.config), telegramCopy: sc.config.telegram.enabled, ttlSec: sc.config.ttlSec, problems: sc.problems }; })(),
|
|
324
326
|
};
|
|
325
327
|
if (argv.includes('--json')) {
|
|
326
328
|
io.stdout.write(`${JSON.stringify(view, null, 2)}\n`);
|
|
@@ -332,6 +334,7 @@ export async function telegramCommand(argv, io, deps = {}) {
|
|
|
332
334
|
io.stdout.write(` approvals ${t.approvals} · expiry ${t.approvalExpirySec} s · re-quote tolerance ${t.repriceTolPct}%\n`);
|
|
333
335
|
io.stdout.write(` notify candidates ${t.notify.candidates} · entries ${t.notify.entries ? 'on' : 'off'} · closes ${t.notify.closes ? 'on' : 'off'} · halts ${t.notify.halts ? 'on' : 'off'} · spend ${t.notify.spend ? 'on' : 'off'} · daily ${t.notify.daily ? `at ${t.dailySummaryAt ?? 'off'}` : 'off'}${t.quietHours ? ` · quiet ${t.quietHours}` : ''}\n`);
|
|
334
336
|
io.stdout.write(` audit ${chain.length} day file${chain.length === 1 ? '' : 's'}${view.audit.broken.length ? ` · BROKEN: ${view.audit.broken.join(', ')}` : ''}\n`);
|
|
337
|
+
io.stdout.write(` trade-fire signals (real mode) · channels ${view.signals.channels.filter((c) => c !== 'telegram').join(', ') || 'none'} · Telegram copy ${view.signals.telegramCopy ? 'on' : 'off'} · ttl ${view.signals.ttlSec} s · set with dexterm autotrade signal channels${view.signals.problems.length ? ` · OFF: ${view.signals.problems.join('; ')}` : ''}\n`);
|
|
335
338
|
return 0;
|
|
336
339
|
}
|
|
337
340
|
case 'approvals': {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The one place the app's version lives (task-1397). --version, the help banner and `dexterm update` read it;
|
|
2
2
|
// cli.test.ts keeps it equal to packages/cli/package.json (release.yml refuses a tag that differs).
|
|
3
|
-
export const VERSION = '0.1
|
|
3
|
+
export const VERSION = '0.2.1';
|