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,217 @@
|
|
|
1
|
+
// `dexterm dash` (task-1749): the DATA behind the strategy performance dashboard. Read-only, local, no network:
|
|
2
|
+
// - every autotrade home given (a DEXTERM_HOME): its run ledgers (autotrade/runs/*.jsonl) through the strategy-stats
|
|
3
|
+
// definitions (@dexterm/autotrade strategy-stats.ts: the challenge control's math), its service heartbeat and the
|
|
4
|
+
// fleet status (open positions with the engine's mark, spend, halts)
|
|
5
|
+
// - the market cache DB (read-only) for the last pool liquidity of an open coin, so an open position shows its
|
|
6
|
+
// REALIZABLE value next to the engine mark, with the age of that pool read; nothing is fetched
|
|
7
|
+
// - optional "books" files (dexterm-dash-books/1): other books (e.g. a SOL challenge run by hand or by an agent),
|
|
8
|
+
// labels, groups, a baseline and status cards, produced by the user's own scripts; their trades go through the
|
|
9
|
+
// same definitions
|
|
10
|
+
// The output never carries an absolute path, a key, a wallet address or a token: homes are named by their label.
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { basename, dirname, join } from 'node:path';
|
|
13
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
14
|
+
import { readStrategyLedgers, strategyRow, equityCurve, compareToBase, decisionRule, realizableUsd, MIN_CLOSED, RUG_PCT, STUCK_WRITE_OFF_MIN, } from '../../../autotrade/src/strategy-stats.js';
|
|
15
|
+
export const PAPER_LABEL = 'Paper trading, not financial advice.';
|
|
16
|
+
export const DASH_SCHEMA = 'dexterm-dash/1';
|
|
17
|
+
export const BOOKS_SCHEMA = 'dexterm-dash-books/1';
|
|
18
|
+
const readJson = (p) => { try {
|
|
19
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
} };
|
|
24
|
+
const fin = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
25
|
+
const r2 = (x) => Math.round(x * 100) / 100;
|
|
26
|
+
const r1 = (x) => Math.round(x * 10) / 10;
|
|
27
|
+
const alive = (pid) => { if (!fin(pid))
|
|
28
|
+
return false; try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return e.code === 'EPERM';
|
|
34
|
+
} };
|
|
35
|
+
/** A home's label: the folder that holds `.dexterm` (e.g. c100-control), else the folder's own name. */
|
|
36
|
+
export function homeLabel(dir) { const b = basename(dir); return b === '.dexterm' ? basename(dirname(dir)) || b : b; }
|
|
37
|
+
/** The last cached pool read of each mint (ns ds:token) from the market cache DB, read-only. Missing DB: empty. */
|
|
38
|
+
export function cachedPools(dbPath, mints) {
|
|
39
|
+
const out = new Map();
|
|
40
|
+
if (!dbPath || !mints.length || !existsSync(dbPath))
|
|
41
|
+
return out;
|
|
42
|
+
let db = null;
|
|
43
|
+
try {
|
|
44
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
45
|
+
const q = db.prepare('SELECT json, fetched_at FROM cache WHERE ns = ? AND key = ?');
|
|
46
|
+
for (const m of new Set(mints)) {
|
|
47
|
+
const r = q.get('ds:token', m);
|
|
48
|
+
if (!r)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
const p = JSON.parse(r.json)?.pair;
|
|
52
|
+
if (fin(p?.liquidityUsd))
|
|
53
|
+
out.set(m, { liqUsd: p.liquidityUsd, at: fin(p.fetchedAt) ? p.fetchedAt : r.fetched_at });
|
|
54
|
+
}
|
|
55
|
+
catch { /* torn row */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { /* locked or not a dexterm cache: no pool reads */ }
|
|
59
|
+
finally {
|
|
60
|
+
try {
|
|
61
|
+
db?.close();
|
|
62
|
+
}
|
|
63
|
+
catch { /* closed */ }
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** The service heartbeat + fleet status of one home as a status card (no paths, no pids beyond alive/dead). */
|
|
68
|
+
export function homeStatus(h, now) {
|
|
69
|
+
const at = join(h.dir, 'autotrade');
|
|
70
|
+
const hb = readJson(join(at, 'service', 'heartbeat.json'));
|
|
71
|
+
const fl = readJson(join(at, 'fleet-status.json'));
|
|
72
|
+
const items = [];
|
|
73
|
+
let state = 'idle';
|
|
74
|
+
if (hb) {
|
|
75
|
+
const age = fin(hb.heartbeatAt) ? Math.round((now - hb.heartbeatAt) / 1000) : null;
|
|
76
|
+
const up = hb.state === 'running' && alive(hb.pid) && age !== null && age < 120;
|
|
77
|
+
state = up ? 'ok' : hb.state === 'running' ? 'down' : 'idle';
|
|
78
|
+
items.push(['Service', up ? 'running' : hb.state === 'running' ? 'NOT running (stale heartbeat)' : String(hb.state ?? 'unknown')]);
|
|
79
|
+
if (age !== null)
|
|
80
|
+
items.push(['Heartbeat', `${age < 120 ? `${age} s` : `${Math.round(age / 60)} min`} ago`]);
|
|
81
|
+
if (fin(hb.dataAgeMs))
|
|
82
|
+
items.push(['Market data age', `${Math.round(hb.dataAgeMs / 1000)} s`]);
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
items.push(['Service', 'no service heartbeat (not installed or never run)']);
|
|
86
|
+
if (fl) {
|
|
87
|
+
const ms = fl.members ?? [];
|
|
88
|
+
if (fin(fl.startedAt))
|
|
89
|
+
items.push(['Fleet started', new Date(fl.startedAt).toISOString()]);
|
|
90
|
+
if (fl.haltReason) {
|
|
91
|
+
items.push(['Fleet halt', String(fl.haltReason)]);
|
|
92
|
+
if (state === 'ok')
|
|
93
|
+
state = 'warn';
|
|
94
|
+
}
|
|
95
|
+
const running = ms.filter((m) => m.state === 'running').length;
|
|
96
|
+
items.push(['Strategies running', `${running} of ${ms.length}`]);
|
|
97
|
+
const halted = ms.filter((m) => m.haltReason).map((m) => `${m.id}: ${m.haltReason}`);
|
|
98
|
+
if (halted.length) {
|
|
99
|
+
items.push(['Halted', halted.join('; ')]);
|
|
100
|
+
if (state === 'ok')
|
|
101
|
+
state = 'warn';
|
|
102
|
+
}
|
|
103
|
+
const spend = ms.reduce((s, m) => s + (fin(m.spend?.todayUsd) ? m.spend.todayUsd : 0), 0);
|
|
104
|
+
items.push(['Model spend today', `$${spend.toFixed(2)}`]);
|
|
105
|
+
}
|
|
106
|
+
return { title: `Autotrade · ${h.label}`, state, items, updatedAt: fin(hb?.heartbeatAt) ? hb.heartbeatAt : null };
|
|
107
|
+
}
|
|
108
|
+
function openFromFleet(h, now) {
|
|
109
|
+
const fl = readJson(join(h.dir, 'autotrade', 'fleet-status.json'));
|
|
110
|
+
const out = new Map();
|
|
111
|
+
if (!fl?.members)
|
|
112
|
+
return out;
|
|
113
|
+
const mints = fl.members.flatMap((m) => (m.open ?? []).map((o) => o.mint)).filter(Boolean);
|
|
114
|
+
const pools = cachedPools(h.db, mints);
|
|
115
|
+
for (const m of fl.members) {
|
|
116
|
+
out.set(m.id, (m.open ?? []).map((o) => {
|
|
117
|
+
const cost = fin(o.costUsd) ? o.costUsd : null;
|
|
118
|
+
const mark = cost !== null ? cost + (fin(o.pnlUsd) ? o.pnlUsd : 0) : null;
|
|
119
|
+
const pool = o.mint ? pools.get(o.mint) : undefined;
|
|
120
|
+
// a stuck position has no pool to sell into: realizable 0 whatever the last read said
|
|
121
|
+
const rv = o.stuck ? 0 : mark !== null ? realizableUsd(mark, pool?.liqUsd) : null;
|
|
122
|
+
return { symbol: o.symbol ?? null, mint: o.mint ?? null, profile: o.profile ?? null, costUsd: cost, markUsd: mark === null ? null : r2(mark), markPct: fin(o.pnlPct) ? o.pnlPct : null, stuck: !!o.stuck,
|
|
123
|
+
liqUsd: pool ? Math.round(pool.liqUsd) : null, liqAt: pool?.at ?? null, realizableUsd: rv === null ? null : r2(rv), realizablePct: rv === null || !cost ? null : r1((rv / cost - 1) * 100), openedAt: null };
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
const rugsOf = (closed) => closed.filter((c) => c.writtenOff || (fin(c.pnlPct) && c.pnlPct <= RUG_PCT) || (c.stuck && !c.writtenOff));
|
|
129
|
+
function fromLedger(id, m, h, fleetOpen, now) {
|
|
130
|
+
const row = strategyRow(id, m, { now });
|
|
131
|
+
// the engine's book is the live truth for what is open; the ledger fold is the fallback when no fleet status exists
|
|
132
|
+
// a stuck bag the engine still holds but the ledger fold already wrote off is CLOSED here (at its realizable value),
|
|
133
|
+
// so it is not listed open a second time
|
|
134
|
+
const heldOff = new Set(m.writtenOff.filter((w) => w.held).map((w) => w.mint));
|
|
135
|
+
const open = fleetOpen ? fleetOpen.filter((o) => !(o.stuck && heldOff.has(o.mint))) : m.openPositions.map((p) => ({ symbol: p.symbol, mint: p.mint, profile: p.profile, costUsd: p.costUsd, markUsd: null, markPct: null, stuck: p.stuckSince !== null, liqUsd: null, liqAt: null, realizableUsd: p.stuckSince !== null ? r2(p.realizedUsd) : null, realizablePct: null, openedAt: p.openedAt }));
|
|
136
|
+
return { id, label: m.basedOn?.version ? `v${m.basedOn.version}` : '', group: h.label, source: 'autotrade', home: h.label, unit: 'USD', mode: m.mode, version: m.basedOn?.version ?? null, baseline: false,
|
|
137
|
+
row, equity: equityCurve(m.closed, m.startUsd), trades: m.closed, open, rugs: rugsOf(m.closed), lastEventAt: m.lastT, firstEventAt: m.firstT };
|
|
138
|
+
}
|
|
139
|
+
function fromBook(b, now) {
|
|
140
|
+
const closed = (b.trades ?? []).filter((t) => fin(t.t) && fin(t.pnlPct)).map((t) => ({ t: t.t, openedAt: t.openedAt ?? null, mint: t.mint ?? null, symbol: t.symbol ?? null, pnlPct: t.pnlPct, pnlUsd: fin(t.pnl) ? t.pnl : fin(t.pnlUsd) ? t.pnlUsd : null,
|
|
141
|
+
costUsd: fin(t.cost) ? t.cost : null, exitReason: t.exitReason ?? null, stuck: !!t.writtenOff, writtenOff: !!t.writtenOff, reason: t.reason, profile: t.profile ?? null, entryAgeMin: fin(t.entryAgeMin) ? t.entryAgeMin : null }))
|
|
142
|
+
.sort((a, b2) => a.t - b2.t);
|
|
143
|
+
const ts = closed.map((c) => c.openedAt ?? c.t);
|
|
144
|
+
const firstT = fin(b.firstT) ? b.firstT : ts.length ? Math.min(...ts) : null;
|
|
145
|
+
const open = (b.open ?? []).map((o) => ({ symbol: o.symbol ?? null, mint: o.mint ?? null, profile: o.profile ?? null, costUsd: fin(o.cost) ? o.cost : null, markUsd: fin(o.mark) ? o.mark : null, markPct: fin(o.markPct) ? o.markPct : null, stuck: !!o.stuck,
|
|
146
|
+
liqUsd: null, liqAt: null, realizableUsd: fin(o.realizable) ? o.realizable : null, realizablePct: fin(o.realizable) && fin(o.cost) && o.cost ? r1((o.realizable / o.cost - 1) * 100) : null, openedAt: o.openedAt ?? null }));
|
|
147
|
+
const m = { entries: fin(b.entries) ? b.entries : closed.length + open.length, closed, open: open.length, writtenOff: closed.filter((c) => c.writtenOff), stuckOpen: [], abandoned: [], adopted: 0, firstT };
|
|
148
|
+
return { id: b.id, label: b.label ?? '', group: b.group ?? 'books', source: 'book', home: b.group ?? 'books', unit: b.unit ?? 'USD', mode: b.mode ?? 'paper', version: b.version ?? null, baseline: false,
|
|
149
|
+
row: strategyRow(b.id, m, { now }), equity: equityCurve(closed, fin(b.start) ? b.start : null), trades: closed, open, rugs: rugsOf(closed),
|
|
150
|
+
lastEventAt: closed.length ? closed[closed.length - 1].t : firstT, firstEventAt: firstT };
|
|
151
|
+
}
|
|
152
|
+
export function readBooksFile(p) {
|
|
153
|
+
const j = readJson(p);
|
|
154
|
+
return j && j.schema === BOOKS_SCHEMA ? j : null;
|
|
155
|
+
}
|
|
156
|
+
/** Everything the dashboard shows, from the homes and books files given. */
|
|
157
|
+
export function collectDash({ homes, books = [], baseline = null, now = Date.now() }) {
|
|
158
|
+
const strategies = [];
|
|
159
|
+
const status = [];
|
|
160
|
+
const notes = [];
|
|
161
|
+
const labels = {};
|
|
162
|
+
const groups = {};
|
|
163
|
+
let order = [];
|
|
164
|
+
let base = baseline;
|
|
165
|
+
for (const h of homes) {
|
|
166
|
+
const runs = join(h.dir, 'autotrade', 'runs');
|
|
167
|
+
if (!existsSync(runs)) {
|
|
168
|
+
notes.push(`${h.label}: no autotrade run ledgers yet`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const M = readStrategyLedgers(runs, { now });
|
|
172
|
+
const fo = openFromFleet(h, now);
|
|
173
|
+
for (const [id, m] of Object.entries(M))
|
|
174
|
+
strategies.push(fromLedger(id, m, h, fo.get(id), now));
|
|
175
|
+
status.push(homeStatus(h, now));
|
|
176
|
+
}
|
|
177
|
+
for (const f of books) {
|
|
178
|
+
const b = readBooksFile(f);
|
|
179
|
+
if (!b) {
|
|
180
|
+
notes.push(`books file ${basename(f)}: missing or not ${BOOKS_SCHEMA}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
for (const x of b.books ?? [])
|
|
184
|
+
strategies.push(fromBook(x, now));
|
|
185
|
+
Object.assign(labels, b.labels ?? {});
|
|
186
|
+
Object.assign(groups, b.groups ?? {});
|
|
187
|
+
if (b.order)
|
|
188
|
+
order = [...order, ...b.order];
|
|
189
|
+
base ??= b.baseline ?? null;
|
|
190
|
+
status.unshift(...(b.status ?? [])); // the books' own cards first (what the user runs by hand)
|
|
191
|
+
notes.push(...(b.notes ?? []));
|
|
192
|
+
}
|
|
193
|
+
for (const s of strategies) {
|
|
194
|
+
if (labels[s.id])
|
|
195
|
+
s.label = labels[s.id];
|
|
196
|
+
if (groups[s.id])
|
|
197
|
+
s.group = groups[s.id];
|
|
198
|
+
s.baseline = s.id === base;
|
|
199
|
+
}
|
|
200
|
+
const rank = (id) => { const i = order.indexOf(id); return i < 0 ? 1e6 : i; };
|
|
201
|
+
strategies.sort((a, b) => Number(b.baseline) - Number(a.baseline) || rank(a.id) - rank(b.id) || a.group.localeCompare(b.group) || a.id.localeCompare(b.id));
|
|
202
|
+
const baseS = strategies.find((s) => s.baseline) ?? null;
|
|
203
|
+
const pcts = (s) => s.trades.map((c) => c.pnlPct).filter(fin);
|
|
204
|
+
const comparisons = baseS ? strategies.filter((s) => s !== baseS).map((s) => compareToBase(s.id, pcts(s), baseS.id, pcts(baseS))) : [];
|
|
205
|
+
return {
|
|
206
|
+
schema: DASH_SCHEMA, generatedAt: now, paper: PAPER_LABEL,
|
|
207
|
+
definitions: { minClosed: MIN_CLOSED, rugPct: RUG_PCT, writeOffMin: STUCK_WRITE_OFF_MIN, text: [
|
|
208
|
+
`Closed = trades the engine closed plus stuck positions written off: no sell for >= ${STUCK_WRITE_OFF_MIN} min (or dropped stuck at a run end) closes at what was actually sold; the unsold rest counts 0.`,
|
|
209
|
+
`Mean % on cost = the average return per closed trade, with a 95% bootstrap interval (4000 resamples). Wide interval = the data cannot tell yet.`,
|
|
210
|
+
`Rug = a closed trade at ${RUG_PCT}% or worse on cost. Rug rate interval: Wilson 95%.`,
|
|
211
|
+
`Under ${MIN_CLOSED} closed trades a strategy is EXPLORATORY: read nothing into it. ETA to ${MIN_CLOSED} = the closed trades still needed / entries per day so far.`,
|
|
212
|
+
`Realizable = the engine's mark sold into the last pool read from the local cache (impact on half the liquidity, 2.3% fees and slippage); a stuck position is 0.`,
|
|
213
|
+
`Equity = the starting book plus realized P/L in close order; drawdown is from the running peak.`,
|
|
214
|
+
] },
|
|
215
|
+
baseline: baseS?.id ?? null, decisionRule: baseS ? decisionRule(baseS.id) : null, strategies, comparisons, status, notes,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// `dexterm dash` (task-1749): the loopback server and the static export of the strategy performance dashboard.
|
|
2
|
+
// Binds 127.0.0.1 only. GET/HEAD only. Serves four files from assets/dash plus /api/dash.json (recomputed at most every
|
|
3
|
+
// few seconds). A Host header outside the allow list is refused (DNS-rebinding guard: a web page cannot read the
|
|
4
|
+
// dashboard through a hostname that resolves to 127.0.0.1); a private proxy in front (e.g. `tailscale serve`) is
|
|
5
|
+
// allowed by naming its host with --allow-host. No external request is made or referenced: the page has no CDN, font
|
|
6
|
+
// or analytics URL, and the CSP forbids any.
|
|
7
|
+
import { createServer } from 'node:http';
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { collectDash } from "./data.js";
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export const ASSET_DIR = resolve(HERE, '..', '..', 'assets', 'dash');
|
|
14
|
+
export const ASSETS = { '/': 'index.html', '/index.html': 'index.html', '/dash.css': 'dash.css', '/dash.js': 'dash.js', '/icon.svg': 'icon.svg' };
|
|
15
|
+
const TYPES = { html: 'text/html; charset=utf-8', css: 'text/css; charset=utf-8', js: 'text/javascript; charset=utf-8', svg: 'image/svg+xml', json: 'application/json; charset=utf-8' };
|
|
16
|
+
export const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
17
|
+
/** Is this Host header one we serve? Loopback names on any port, plus the hosts allowed explicitly (with or without a port). */
|
|
18
|
+
export function hostAllowed(host, allow) {
|
|
19
|
+
if (!host)
|
|
20
|
+
return false;
|
|
21
|
+
const h = host.toLowerCase();
|
|
22
|
+
const name = h.startsWith('[') ? h.slice(0, h.indexOf(']') + 1) : h.replace(/:\d+$/, '');
|
|
23
|
+
if (name === '127.0.0.1' || name === 'localhost' || name === '[::1]')
|
|
24
|
+
return true;
|
|
25
|
+
return allow.some((a) => { const x = a.toLowerCase(); return x === h || x === name; });
|
|
26
|
+
}
|
|
27
|
+
export function startDashServer(opts, port) {
|
|
28
|
+
let cache = null;
|
|
29
|
+
const data = () => {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (!cache || now - cache.at > (opts.cacheMs ?? 5000))
|
|
32
|
+
cache = { at: now, body: JSON.stringify(collectDash({ homes: opts.homes, books: opts.books, baseline: opts.baseline, now })) };
|
|
33
|
+
return cache.body;
|
|
34
|
+
};
|
|
35
|
+
const server = createServer((req, res) => {
|
|
36
|
+
const base = { 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'Cache-Control': 'no-store' };
|
|
37
|
+
if (!hostAllowed(req.headers.host, opts.allowHosts)) {
|
|
38
|
+
res.writeHead(421, { ...base, 'Content-Type': 'text/plain' });
|
|
39
|
+
res.end('misdirected request: this host name is not allowed (dexterm dash --allow-host NAME)\n');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
43
|
+
res.writeHead(405, { ...base, Allow: 'GET, HEAD' });
|
|
44
|
+
res.end();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const path = (req.url ?? '/').split('?')[0];
|
|
48
|
+
try {
|
|
49
|
+
if (path === '/api/dash.json') {
|
|
50
|
+
const body = data();
|
|
51
|
+
res.writeHead(200, { ...base, 'Content-Type': TYPES.json });
|
|
52
|
+
res.end(req.method === 'HEAD' ? undefined : body);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const file = ASSETS[path];
|
|
56
|
+
if (!file) {
|
|
57
|
+
res.writeHead(404, { ...base, 'Content-Type': 'text/plain' });
|
|
58
|
+
res.end('not found\n');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const body = readFileSync(join(ASSET_DIR, file));
|
|
62
|
+
res.writeHead(200, { ...base, 'Content-Type': TYPES[file.split('.').pop()] ?? 'application/octet-stream' });
|
|
63
|
+
res.end(req.method === 'HEAD' ? undefined : body);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
res.writeHead(500, { ...base, 'Content-Type': 'text/plain' });
|
|
67
|
+
res.end(`dashboard error: ${String(e?.message ?? e).slice(0, 200)}\n`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return new Promise((ok, fail) => { server.once('error', fail); server.listen(port, '127.0.0.1', () => { server.off('error', fail); ok(server); }); });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A static, read-only SNAPSHOT: the same page + the data as data.json (the page reads it instead of /api/dash.json and
|
|
74
|
+
* shows "snapshot" with its time, no auto-refresh). `redactMints` drops the mints (coin symbols stay).
|
|
75
|
+
*/
|
|
76
|
+
export function exportSnapshot(d, outDir, { redactMints = false } = {}) {
|
|
77
|
+
mkdirSync(outDir, { recursive: true });
|
|
78
|
+
const data = redactMints ? JSON.parse(JSON.stringify(d, (k, v) => (k === 'mint' ? null : v))) : d;
|
|
79
|
+
const written = [];
|
|
80
|
+
for (const f of new Set(Object.values(ASSETS))) {
|
|
81
|
+
let body = readFileSync(join(ASSET_DIR, f), 'utf8');
|
|
82
|
+
if (f === 'index.html')
|
|
83
|
+
body = body.replace('content="api/dash.json"', 'content="data.json"').replace('data-mode="live"', 'data-mode="snapshot"');
|
|
84
|
+
writeFileSync(join(outDir, f), body);
|
|
85
|
+
written.push(f);
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(join(outDir, 'data.json'), JSON.stringify({ ...data, snapshot: true }));
|
|
88
|
+
written.push('data.json');
|
|
89
|
+
writeFileSync(join(outDir, '_headers'), `/*\n Content-Security-Policy: ${CSP}\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`);
|
|
90
|
+
written.push('_headers');
|
|
91
|
+
return written;
|
|
92
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `dexterm dash` (task-1749): the STRATEGY PERFORMANCE DASHBOARD. Local-first: it reads your own autotrade run ledgers
|
|
2
|
+
// ($DEXTERM_HOME/autotrade/runs) and serves a page on 127.0.0.1 only; no account, no cloud, nothing leaves the machine.
|
|
3
|
+
// Read-only: it never writes into the engine's state. PAPER results are simulated; not financial advice.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { dextermHome } from '../../custom/src/index.js';
|
|
7
|
+
import { defaultDbPath } from '../../data/src/index.js';
|
|
8
|
+
import { collectDash, homeLabel } from "./dash/data.js";
|
|
9
|
+
import { exportSnapshot, startDashServer } from "./dash/server.js";
|
|
10
|
+
export const DASH_PORT = 4317;
|
|
11
|
+
export const DASH_HELP = `dexterm dash: track how your autotrade strategies perform, in a browser, on this machine only. Read-only.
|
|
12
|
+
|
|
13
|
+
dexterm dash serve the dashboard on http://127.0.0.1:${DASH_PORT}/ (Ctrl-C stops it)
|
|
14
|
+
dexterm dash --json print the numbers the page shows and exit
|
|
15
|
+
dexterm dash --export DIR [--redact-mints] write a static read-only snapshot (the page + data.json) and exit
|
|
16
|
+
|
|
17
|
+
--port N the loopback port (default ${DASH_PORT}; 0 = any free port)
|
|
18
|
+
--home DIR a dexterm home to read (default $DEXTERM_HOME or ~/.dexterm); repeat for several
|
|
19
|
+
--db PATH the market cache DB for pool reads (default: dexterm.db next to a .dexterm home, else DEXTERM_DB_PATH or the default DB)
|
|
20
|
+
--books FILE add books from a dexterm-dash-books/1 JSON file (your own script's output); repeat for several
|
|
21
|
+
--baseline ID the strategy every other one is compared against
|
|
22
|
+
--allow-host NAME also answer requests for this host name (a private proxy in front, e.g. tailscale serve)
|
|
23
|
+
|
|
24
|
+
Overview, strategy detail, rugs & stuck, compare (vs the baseline, with the 95% interval) and live status, refreshed
|
|
25
|
+
every 30 s. The server binds 127.0.0.1 only, makes no network request and loads nothing from the internet.
|
|
26
|
+
PAPER results are simulated. Not financial advice.`;
|
|
27
|
+
const VALUE = new Set(['port', 'home', 'db', 'books', 'baseline', 'allow-host', 'export']);
|
|
28
|
+
const BOOL = new Set(['json', 'help', 'redact-mints']);
|
|
29
|
+
function parse(argv) {
|
|
30
|
+
const o = { _: [], flags: new Map(), bools: new Set() };
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a === '-h') {
|
|
34
|
+
o.bools.add('help');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!a.startsWith('--')) {
|
|
38
|
+
o._.push(a);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
|
|
42
|
+
if (BOOL.has(k)) {
|
|
43
|
+
o.bools.add(k);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!VALUE.has(k))
|
|
47
|
+
return `unknown flag --${k}`;
|
|
48
|
+
const v = inline ?? argv[++i];
|
|
49
|
+
if (v === undefined || v === '')
|
|
50
|
+
return `--${k} needs a value`;
|
|
51
|
+
o.flags.set(k, [...(o.flags.get(k) ?? []), v]);
|
|
52
|
+
}
|
|
53
|
+
return o;
|
|
54
|
+
}
|
|
55
|
+
/** A home given as DIR: the .dexterm folder itself, or a folder holding one (the kit's layout: <dir>/.dexterm + <dir>/dexterm.db). */
|
|
56
|
+
export function resolveHome(dir, env, db) {
|
|
57
|
+
let d = resolve(dir);
|
|
58
|
+
if (basename(d) !== '.dexterm' && !existsSync(join(d, 'autotrade')) && existsSync(join(d, '.dexterm')))
|
|
59
|
+
d = join(d, '.dexterm');
|
|
60
|
+
const sibling = basename(d) === '.dexterm' ? join(dirname(d), 'dexterm.db') : null;
|
|
61
|
+
return { dir: d, label: homeLabel(d), db: db ?? (sibling && existsSync(sibling) ? sibling : env.DEXTERM_DB_PATH || defaultDbPath(env)) };
|
|
62
|
+
}
|
|
63
|
+
export async function dashCommand(argv, io, { wait = true } = {}) {
|
|
64
|
+
const o = parse(argv);
|
|
65
|
+
if (typeof o === 'string') {
|
|
66
|
+
io.stderr.write(`dexterm dash: ${o}\n\n${DASH_HELP}\n`);
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
if (o.bools.has('help') || o._[0] === 'help') {
|
|
70
|
+
io.stdout.write(`${DASH_HELP}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (o._.length) {
|
|
74
|
+
io.stderr.write(`dexterm dash: unexpected argument ${o._[0]}\n\n${DASH_HELP}\n`);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
const db = o.flags.get('db')?.[0];
|
|
78
|
+
const homes = (o.flags.get('home') ?? [dextermHome(io.env)]).map((h) => resolveHome(h, io.env, db));
|
|
79
|
+
const books = (o.flags.get('books') ?? []).map((b) => resolve(b));
|
|
80
|
+
const baseline = o.flags.get('baseline')?.[0] ?? null;
|
|
81
|
+
if (o.bools.has('json')) {
|
|
82
|
+
io.stdout.write(`${JSON.stringify(collectDash({ homes, books, baseline }), null, 1)}\n`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const out = o.flags.get('export')?.[0];
|
|
86
|
+
if (out) {
|
|
87
|
+
const files = exportSnapshot(collectDash({ homes, books, baseline }), resolve(out), { redactMints: o.bools.has('redact-mints') });
|
|
88
|
+
io.stdout.write(`snapshot written to ${resolve(out)}: ${files.join(', ')}\nIt is a static read-only copy of what this machine shows now. Publishing it is your decision; check it first. Not financial advice.\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const port = Number(o.flags.get('port')?.[0] ?? DASH_PORT);
|
|
92
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
93
|
+
io.stderr.write(`dexterm dash: --port must be 0-65535\n`);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
let server;
|
|
97
|
+
try {
|
|
98
|
+
server = await startDashServer({ homes, books, baseline, allowHosts: o.flags.get('allow-host') ?? [] }, port);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
io.stderr.write(`dexterm dash: cannot listen on 127.0.0.1:${port} (${e.code ?? e}); try --port N\n`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
const addr = server.address();
|
|
105
|
+
const p = typeof addr === 'object' && addr ? addr.port : port;
|
|
106
|
+
io.stdout.write(`dexterm dash: http://127.0.0.1:${p}/ (reading ${homes.map((h) => h.label).join(', ')}${books.length ? ` + ${books.length} books file(s)` : ''}; loopback only, read-only; Ctrl-C stops)\nPaper trading, not financial advice.\n`);
|
|
107
|
+
if (!wait) {
|
|
108
|
+
server.close();
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
await new Promise((done) => { const stop = () => { server.close(() => done()); }; process.once('SIGINT', stop); process.once('SIGTERM', stop); });
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
@@ -204,6 +204,7 @@ export function defaultDeps(env = process.env, isTTY = process.stdout.isTTY ===
|
|
|
204
204
|
claude: () => claudeProbe(),
|
|
205
205
|
codex: () => codexProbe(4000, env),
|
|
206
206
|
ollama: () => ollamaFacts(env),
|
|
207
|
+
classifier: () => classifierFacts(env),
|
|
207
208
|
analystEngine: env.DEXTERM_ANALYST_ENGINE ?? loadConfig(dextermHome(env)).config.analyst?.engine ?? null,
|
|
208
209
|
probe: (url) => livingProbe(url),
|
|
209
210
|
checkWritable: writableProbe,
|
|
@@ -325,6 +326,12 @@ export async function runDoctor(deps) {
|
|
|
325
326
|
// 3c. Local engine (task-1543): the user's own Ollama on this machine, the model, tool calling, context, profile.
|
|
326
327
|
if (ol)
|
|
327
328
|
checks.push(ollamaCheck(ol, claude !== 'missing' || codexUsable));
|
|
329
|
+
// 3d. Classification model (task-1822, EXPERIMENTAL): hardware tier + the picked model. Never fails the doctor.
|
|
330
|
+
if (deps.classifier) {
|
|
331
|
+
const cf = await deps.classifier().catch(() => null);
|
|
332
|
+
if (cf)
|
|
333
|
+
checks.push(classifierCheck(cf));
|
|
334
|
+
}
|
|
328
335
|
// 4b. Login with X (optional, task-1357): the user's own X app + login unlock X posts in the SOCIAL panel / TRENDING.
|
|
329
336
|
{
|
|
330
337
|
const xs = new XTokenStore({ env: deps.env });
|
|
@@ -471,6 +478,33 @@ export function renderDoctor(result, opts = {}) {
|
|
|
471
478
|
lines.push(paint(`${fails} check${fails === 1 ? '' : 's'} failed — fix the ✗ lines above, then re-run \`dexterm doctor\`.`, color ? ANSI.fail : ''));
|
|
472
479
|
return lines.join('\n');
|
|
473
480
|
}
|
|
481
|
+
/** task-1822: what `dexterm classifier doctor` says, as one doctor line. */
|
|
482
|
+
export async function classifierFacts(env = process.env) {
|
|
483
|
+
const { describeHardware, detectHardware, loadClassifierConfig, providerFromConfig, recommendTier } = await import('../../classifier/src/index.js');
|
|
484
|
+
const hw = await detectHardware();
|
|
485
|
+
const rec = recommendTier(hw);
|
|
486
|
+
const c = loadClassifierConfig(env).config;
|
|
487
|
+
let st = { ready: false, detail: '' };
|
|
488
|
+
let model = c.provider;
|
|
489
|
+
try {
|
|
490
|
+
const p = providerFromConfig(c, c.provider, { env });
|
|
491
|
+
model = p.model();
|
|
492
|
+
st = await p.status();
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
st = { ready: false, detail: e.message };
|
|
496
|
+
}
|
|
497
|
+
return { hardware: describeHardware(hw), recommended: rec.model, bestQuality: rec.bestQuality, why: rec.why, provider: c.provider, model, ready: st.ready, detail: st.detail };
|
|
498
|
+
}
|
|
499
|
+
export function classifierCheck(f) {
|
|
500
|
+
const tier = f.recommended ? `recommended model here: ${f.recommended}${f.bestQuality ? ` (best quality: ${f.bestQuality})` : ''}` : 'recommended: the built-in scores';
|
|
501
|
+
const name = 'Classification model (experimental)';
|
|
502
|
+
if (f.provider === 'heuristic')
|
|
503
|
+
return { name, status: 'pass', detail: `built-in scores (no download) · ${f.hardware} · ${tier}` };
|
|
504
|
+
if (f.ready)
|
|
505
|
+
return { name, status: 'pass', detail: `${f.provider} ${f.model} ready · ${tier}` };
|
|
506
|
+
return { name, status: 'warn', detail: `${f.provider} ${f.model} not ready: ${f.detail}`, fix: f.provider === 'kev-local' ? `dexterm classifier pull ${f.model}, then dexterm classifier serve (answers fall back to the built-in scores meanwhile)` : 'dexterm classifier doctor' };
|
|
507
|
+
}
|
|
474
508
|
/**
|
|
475
509
|
* The Ollama line (task-1543). Optional while claude/codex can run the analyst; a warning with the 2-minute
|
|
476
510
|
* setup when nothing can, or when the user chose ollama and it is not ready.
|
|
@@ -20,7 +20,7 @@ export const LEDGER_HELP = `dexterm ledger: the unified trade ledger (paper, age
|
|
|
20
20
|
dexterm ledger sync [--json] append what the sources hold to the ledger file
|
|
21
21
|
dexterm ledger verify [--json] recompute the ledger file's sha256 chain
|
|
22
22
|
|
|
23
|
-
filters: --source paper|desk|autotrade|live --mode paper|devnet|live --strategy ID[@VERSION|@HASH] --profile P
|
|
23
|
+
filters: --source paper|desk|autotrade|live --mode paper|devnet|live|real --strategy ID[@VERSION|@HASH] --profile P
|
|
24
24
|
--exit REASON --mint MINT --run RUN_ID --account ID --days N --status open|closed --wallet-db PATH
|
|
25
25
|
|
|
26
26
|
The file is $DEXTERM_HOME/ledger/trades.jsonl (default ~/.dexterm). PAPER rows are simulated; filter by --mode
|
|
@@ -95,9 +95,11 @@ function banner(v, rows) {
|
|
|
95
95
|
out.push(` ! ${s.problem}`);
|
|
96
96
|
const modes = [...new Set(rows.map((t) => t.walletMode))];
|
|
97
97
|
if (modes.length > 1)
|
|
98
|
-
out.push(' PAPER, DEVNET and LIVE trades are mixed here: use --mode before comparing results.');
|
|
98
|
+
out.push(' PAPER, REAL, DEVNET and LIVE trades are mixed here: use --mode before comparing results.');
|
|
99
99
|
else if (modes[0] === 'paper')
|
|
100
100
|
out.push(' PAPER (simulated) trades.');
|
|
101
|
+
else if (modes[0] === 'real')
|
|
102
|
+
out.push(' REAL-mode trades: dexterm modelled the trades it signalled to your executor; your executor\'s receipts are the real fills.');
|
|
101
103
|
return out;
|
|
102
104
|
}
|
|
103
105
|
function tradesTable(rows) {
|
|
@@ -19,6 +19,7 @@ import { launchIntelCells, holderTags } from "../intel.js";
|
|
|
19
19
|
import { devBookOf, devRows } from "../dev-history.js";
|
|
20
20
|
import { EX_POOL_TAGS } from '../../../../data/src/index.js';
|
|
21
21
|
import { layoutFor } from "../../custom.js";
|
|
22
|
+
import { narrativeCell } from "../../narratives.js";
|
|
22
23
|
import { chartMode, chartTitle, priceModeOf, chartAxisOf, chartMarkersOn, mcapAt, positionPriceCells, levelCell, supplyRatio } from "../../display.js";
|
|
23
24
|
const dash = '—';
|
|
24
25
|
const EX_POOL_TAG_SET = EX_POOL_TAGS;
|
|
@@ -211,6 +212,9 @@ export function renderTokenLive(c, state, v) {
|
|
|
211
212
|
['RugCheck score', ...(v.rug === null || v.rug.score === null ? na(v.panels.rugcheck.state === 'error' ? `unavailable: ${v.panels.rugcheck.reason ?? ''}`.trim() : dash) : [`${Math.round(v.rug.score)}/100`, v.rug.score < 30 ? 'ok' : v.rug.score < 60 ? 'warn' : 'bad'])],
|
|
212
213
|
['GoPlus', ...goplusRow(v)],
|
|
213
214
|
...devTail,
|
|
215
|
+
// task-1818 (EXPERIMENTAL): the coin's narrative from the already-built tracker (measured heat, not advice). Never builds
|
|
216
|
+
// one here (the 26 h replay runs only from N → n); warn = copy-cat saturated or fading/dead, else neutral na (never a green ok).
|
|
217
|
+
['Narrative', ...narrativeCell(state, v.mint)],
|
|
214
218
|
];
|
|
215
219
|
for (const [label, val0, lvl] of fitRows(rows, sb.y + sb.h - sy)) {
|
|
216
220
|
if (sy >= sb.y + sb.h)
|
|
@@ -69,6 +69,8 @@ export class LiveSignalsLive {
|
|
|
69
69
|
logByMint = new Map();
|
|
70
70
|
/** Signals emitted this session (all mints), newest last — a cheap counter/feed for the smoke script + tests. */
|
|
71
71
|
emitted = [];
|
|
72
|
+
/** task-1427: extra read-only listeners (the MCP follow session forwards each signal to the spectator hub). */
|
|
73
|
+
listeners = new Set();
|
|
72
74
|
constructor(opts = {}) {
|
|
73
75
|
this.engine = new LiveSignalsEngine({ thresholds: opts.thresholds });
|
|
74
76
|
this.now = opts.now ?? (() => Date.now());
|
|
@@ -86,6 +88,12 @@ export class LiveSignalsLive {
|
|
|
86
88
|
this.logByMint.set(s.mint, arr);
|
|
87
89
|
this.emitted.push(s);
|
|
88
90
|
this.onSignal?.(s);
|
|
91
|
+
for (const l of this.listeners) {
|
|
92
|
+
try {
|
|
93
|
+
l(s);
|
|
94
|
+
}
|
|
95
|
+
catch { /* a listener never breaks the engine */ }
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
return sigs;
|
|
91
99
|
}
|