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,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dexterm executor reference kit: the JSONL stream tail (task-1654). DRY RUN: logs what it would do; no wallet code.
|
|
3
|
+
//
|
|
4
|
+
// node jsonl-tail-executor.mjs --account main [--stream FILE] [--secret-file FILE --require-hmac] [--once] [--poll-ms 1000]
|
|
5
|
+
//
|
|
6
|
+
// Turn the stream on in dexterm (`dexterm autotrade signal channels --jsonl on`, default file
|
|
7
|
+
// autotrade/real/signals.jsonl). With `--jsonl-signed on` each line is an HMAC envelope this tail verifies; a bare
|
|
8
|
+
// line has no signature, so --require-hmac refuses it. The read offset is saved, so a restart resumes where it was.
|
|
9
|
+
// --once reads what is there and exits (handy for a test).
|
|
10
|
+
|
|
11
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { SeenStore, args, dextermHome, handle, heartbeat, readSecret, realDir, unwrapLine } from './kit.mjs';
|
|
14
|
+
|
|
15
|
+
const a = args();
|
|
16
|
+
if (!a.account) { console.error('usage: node jsonl-tail-executor.mjs --account ID [--stream FILE] [--secret-file FILE] [--once]'); process.exit(2); }
|
|
17
|
+
const home = a.home || dextermHome();
|
|
18
|
+
const stream = a.stream || join(realDir(home), 'signals.jsonl');
|
|
19
|
+
const offFile = a['offset-file'] || join(realDir(home), 'kit-tail-offset.json');
|
|
20
|
+
const seen = new SeenStore(a.state || join(realDir(home), 'kit-tail-seen.json'));
|
|
21
|
+
const secret = readSecret(a['secret-file']);
|
|
22
|
+
const name = String(a.name || 'kit-jsonl-tail').slice(0, 32);
|
|
23
|
+
const log = (s) => console.log(`[${new Date().toISOString()}] ${s}`);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
try { offset = Number(JSON.parse(readFileSync(offFile, 'utf8')).offset) || 0; } catch { /* from the start */ }
|
|
26
|
+
let carry = '';
|
|
27
|
+
|
|
28
|
+
function poll() {
|
|
29
|
+
if (!existsSync(stream)) return;
|
|
30
|
+
const size = statSync(stream).size;
|
|
31
|
+
if (size < offset) { offset = 0; carry = ''; }
|
|
32
|
+
if (size === offset) return;
|
|
33
|
+
const fd = openSync(stream, 'r');
|
|
34
|
+
const buf = Buffer.alloc(size - offset);
|
|
35
|
+
try { readSync(fd, buf, 0, buf.length, offset); } finally { closeSync(fd); }
|
|
36
|
+
offset = size;
|
|
37
|
+
const lines = (carry + buf.toString('utf8')).split('\n');
|
|
38
|
+
carry = lines.pop() ?? '';
|
|
39
|
+
for (const l of lines) {
|
|
40
|
+
if (!l.trim()) continue;
|
|
41
|
+
const { body, signature } = unwrapLine(l);
|
|
42
|
+
handle(body, { accountId: a.account, secret, requireHmac: !!a['require-hmac'], signature, seen, home, executor: name, log, now: Date.now() });
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(offFile, JSON.stringify({ offset: offset - Buffer.byteLength(carry) }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const beat = () => { try { heartbeat(home, { executor: name, accountId: a.account, lastNonce: seen.maxNonce || null }); } catch (e) { log(`heartbeat failed: ${e.message}`); } };
|
|
48
|
+
poll();
|
|
49
|
+
beat();
|
|
50
|
+
if (!a.once) {
|
|
51
|
+
log(`dexterm kit JSONL tail executor (DRY RUN) on ${stream} · account ${a.account}`);
|
|
52
|
+
const t1 = setInterval(poll, Math.max(100, Number(a['poll-ms'] ?? 1000)));
|
|
53
|
+
const t2 = setInterval(beat, Math.max(5, Number(a['heartbeat-sec'] ?? 60)) * 1000);
|
|
54
|
+
const stop = () => { clearInterval(t1); clearInterval(t2); process.exit(0); };
|
|
55
|
+
process.on('SIGINT', stop);
|
|
56
|
+
process.on('SIGTERM', stop);
|
|
57
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// dexterm executor reference kit: the shared part (task-1654). Plain Node (22+), no dependencies, no wallet code.
|
|
2
|
+
//
|
|
3
|
+
// Every kit executor does the same five things with a LIVE TRADE FIRE signal (`dexterm-trade-fire/1`):
|
|
4
|
+
// 1. verify the HMAC (`t=<unix s>,v1=<hex sha256 of "<t>.<body>">`) when you gave it the secret file
|
|
5
|
+
// 2. check the shape, and that the signal is for YOUR account id
|
|
6
|
+
// 3. dedupe by nonce and id (delivery is at-least-once: a repeat is normal); the seen set survives a restart
|
|
7
|
+
// 4. honor the TTL: anything past `expiresAt` is ignored, never acted on late
|
|
8
|
+
// 5. write a receipt (`dexterm-trade-receipt/1`) to dexterm's receipts.jsonl, and a heartbeat now and then
|
|
9
|
+
//
|
|
10
|
+
// It is DRY RUN, always: `act()` below only logs what it WOULD do and the receipt says so (status "rejected",
|
|
11
|
+
// reason "dry-run: …"). There is no key, no wallet and no chain call in this kit. To trade for real you replace
|
|
12
|
+
// `act()` with your own executor's code (or hand the order to the Telegram trading bot you chose); dexterm never
|
|
13
|
+
// signs anything.
|
|
14
|
+
|
|
15
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
export const SIGNAL_SCHEMA = 'dexterm-trade-fire/1';
|
|
21
|
+
export const RECEIPT_SCHEMA = 'dexterm-trade-receipt/1';
|
|
22
|
+
export const HEARTBEAT_SCHEMA = 'dexterm-executor-heartbeat/1';
|
|
23
|
+
export const ENVELOPE_SCHEMA = 'dexterm-trade-fire-envelope/1';
|
|
24
|
+
const TOLERANCE_SEC = 300;
|
|
25
|
+
const MINT = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
26
|
+
|
|
27
|
+
/** `--name value` / `--flag` argv into an object. */
|
|
28
|
+
export function args(argv = process.argv.slice(2)) {
|
|
29
|
+
const o = {};
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (!a.startsWith('--')) continue;
|
|
33
|
+
const k = a.slice(2);
|
|
34
|
+
const v = argv[i + 1];
|
|
35
|
+
if (v !== undefined && !v.startsWith('--')) { o[k] = v; i++; } else o[k] = true;
|
|
36
|
+
}
|
|
37
|
+
return o;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Where dexterm keeps its files (DEXTERM_HOME, else ~/.dexterm). */
|
|
41
|
+
export const dextermHome = (env = process.env) => env.DEXTERM_HOME || join(homedir(), '.dexterm');
|
|
42
|
+
export const realDir = (home) => join(home, 'autotrade', 'real');
|
|
43
|
+
|
|
44
|
+
/** The HMAC secret from a file (`dexterm autotrade signal secret generate --out FILE`), or null. */
|
|
45
|
+
export function readSecret(file) {
|
|
46
|
+
if (!file) return null;
|
|
47
|
+
const v = readFileSync(file, 'utf8').trim();
|
|
48
|
+
return v || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function verify(secret, header, body, nowMs = Date.now()) {
|
|
52
|
+
const m = /^t=(\d{1,12}),v1=([0-9a-f]{64})$/.exec(String(header ?? ''));
|
|
53
|
+
if (!m) return { ok: false, why: 'signature-missing' };
|
|
54
|
+
const t = Number(m[1]);
|
|
55
|
+
if (Math.abs(nowMs / 1000 - t) > TOLERANCE_SEC) return { ok: false, why: 'signature-stale' };
|
|
56
|
+
const want = createHmac('sha256', secret).update(`${t}.${body}`).digest();
|
|
57
|
+
const got = Buffer.from(m[2], 'hex');
|
|
58
|
+
return got.length === want.length && timingSafeEqual(got, want) ? { ok: true } : { ok: false, why: 'signature-bad' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The seen nonces and ids, in a small JSON file so a restart does not act on a repeat. */
|
|
62
|
+
export class SeenStore {
|
|
63
|
+
constructor(file) { this.file = file; this.seen = new Set(); this.maxNonce = 0; try { const d = JSON.parse(readFileSync(file, 'utf8')); for (const x of d.seen ?? []) this.seen.add(x); this.maxNonce = Number(d.maxNonce) || 0; } catch { /* first run */ } }
|
|
64
|
+
has(s) { return this.seen.has(s.id) || this.seen.has(`n${s.nonce}`); }
|
|
65
|
+
add(s) {
|
|
66
|
+
this.seen.add(s.id); this.seen.add(`n${s.nonce}`); this.maxNonce = Math.max(this.maxNonce, s.nonce);
|
|
67
|
+
const keep = [...this.seen].slice(-4000);
|
|
68
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
69
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
70
|
+
writeFileSync(tmp, JSON.stringify({ seen: keep, maxNonce: this.maxNonce }));
|
|
71
|
+
renameSync(tmp, this.file);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check one signal body. Returns { verdict, signal?, why }: verdict is act | duplicate | expired | rejected.
|
|
77
|
+
* `secret` null = no HMAC check (only when you chose not to set one).
|
|
78
|
+
*/
|
|
79
|
+
export function check(body, o) {
|
|
80
|
+
const now = o.now ?? Date.now();
|
|
81
|
+
if (o.secret) { const v = verify(o.secret, o.signature, body, now); if (!v.ok) return { verdict: 'rejected', why: v.why }; }
|
|
82
|
+
else if (o.requireHmac) return { verdict: 'rejected', why: 'signature-required' };
|
|
83
|
+
let s;
|
|
84
|
+
try { s = JSON.parse(body); } catch { return { verdict: 'rejected', why: 'not-json' }; }
|
|
85
|
+
if (!s || s.schema !== SIGNAL_SCHEMA || !['trade', 'halted', 'test'].includes(s.type) || !Number.isSafeInteger(s.nonce) || typeof s.id !== 'string') return { verdict: 'rejected', why: 'shape' };
|
|
86
|
+
if (s.paper !== false) return { verdict: 'rejected', why: 'paper-flag' };
|
|
87
|
+
if (s.accountId !== o.accountId) return { verdict: 'rejected', why: 'wrong-account', signal: s };
|
|
88
|
+
if (o.seen.has(s)) return { verdict: 'duplicate', why: 'duplicate', signal: s };
|
|
89
|
+
if (!(now <= s.expiresAt)) return { verdict: 'expired', why: 'expired', signal: s };
|
|
90
|
+
if (s.type === 'trade' && (!MINT.test(s.mint) || (s.side !== 'buy' && s.side !== 'sell'))) return { verdict: 'rejected', why: 'shape', signal: s };
|
|
91
|
+
if (s.type === 'trade' && s.nonce <= o.seen.maxNonce) return { verdict: 'rejected', why: 'nonce-replay', signal: s };
|
|
92
|
+
return { verdict: 'act', why: null, signal: s };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What the executor does with a fresh signal. DRY RUN: it only says what it WOULD do. Replace this with your own
|
|
97
|
+
* code to trade (your wallet, your bot); keep the rest of the pipeline (verify, dedupe, TTL, receipt) as it is.
|
|
98
|
+
*/
|
|
99
|
+
export function act(s) {
|
|
100
|
+
if (s.type === 'test') return { status: 'rejected', reason: 'test signal: received, never traded' };
|
|
101
|
+
if (s.type === 'halted') return { status: null, reason: `halted (${s.reason}): would cancel ${s.outstanding.length} outstanding signal(s)` };
|
|
102
|
+
const what = s.side === 'buy' ? `would BUY ${s.sizeSol} SOL of ${s.symbol} (${s.mint}) with ${s.slippageBps} bps slippage` : `would SELL ${Math.round(s.sellFraction * 1000) / 10}% of ${s.symbol} (${s.mint}): ${s.exitReason}`;
|
|
103
|
+
return { status: 'rejected', reason: `dry-run: ${what}`.slice(0, 120) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Append a receipt line to dexterm's receipts.jsonl (the same file `dexterm autotrade receipt` writes). */
|
|
107
|
+
export function writeReceipt(home, s, r, executor, now = Date.now()) {
|
|
108
|
+
const line = { schema: RECEIPT_SCHEMA, signalId: s.id, nonce: s.nonce, accountId: s.accountId, status: r.status, at: now, fill: r.fill ?? null, reason: r.reason ?? null, executor };
|
|
109
|
+
mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
|
|
110
|
+
appendFileSync(join(realDir(home), 'receipts.jsonl'), `${JSON.stringify(line)}\n`, { mode: 0o600 });
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Append a heartbeat to dexterm's executor.jsonl: dexterm pauses new buys when it hears nothing for too long. */
|
|
115
|
+
export function heartbeat(home, o) {
|
|
116
|
+
const hb = { schema: HEARTBEAT_SCHEMA, at: o.now ?? Date.now(), executor: o.executor, accountId: o.accountId ?? null, dryRun: true, lastNonce: o.lastNonce ?? null, note: o.note ?? null };
|
|
117
|
+
mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
|
|
118
|
+
appendFileSync(join(realDir(home), 'executor.jsonl'), `${JSON.stringify(hb)}\n`, { mode: 0o600 });
|
|
119
|
+
return hb;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The whole pipeline for one body: check, act (dry run), receipt, log line. Returns the verdict. */
|
|
123
|
+
export function handle(body, o) {
|
|
124
|
+
const c = check(body, o);
|
|
125
|
+
const s = c.signal;
|
|
126
|
+
if (c.verdict === 'act') {
|
|
127
|
+
o.seen.add(s);
|
|
128
|
+
const r = act(s);
|
|
129
|
+
if (r.status && s.type !== 'halted') writeReceipt(o.home, s, r, o.executor, o.now ?? Date.now());
|
|
130
|
+
o.log(`#${s.nonce} ${s.type}${s.type === 'trade' ? ` ${s.side}` : ''}: ${r.reason}`);
|
|
131
|
+
} else o.log(`${s ? `#${s.nonce} ` : ''}${c.verdict}: ${c.why}`);
|
|
132
|
+
return c;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A stream line: a bare signal, or a signed envelope (`dexterm autotrade signal channels --jsonl-signed on`). */
|
|
136
|
+
export function unwrapLine(line) {
|
|
137
|
+
try {
|
|
138
|
+
const x = JSON.parse(line);
|
|
139
|
+
if (x && x.schema === ENVELOPE_SCHEMA && typeof x.signal === 'string') return { body: x.signal, signature: x.signature ?? null };
|
|
140
|
+
} catch { /* handled by check() */ }
|
|
141
|
+
return { body: line, signature: null };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const exists = existsSync;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dexterm executor reference kit: the --signal-cmd hook (task-1654). DRY RUN: logs what it would do; no wallet code.
|
|
3
|
+
//
|
|
4
|
+
// dexterm runs it once per signal, with no shell, the signal line on stdin and the HMAC in DEXTERM_SIGNAL_SIGNATURE
|
|
5
|
+
// (when a secret is set). Wire it with an absolute path:
|
|
6
|
+
// dexterm autotrade signal channels --signal-cmd '["/usr/local/bin/node","/path/to/signal-cmd-executor.mjs","--account","main","--secret-file","/path/to/secret"]'
|
|
7
|
+
//
|
|
8
|
+
// Exit codes dexterm reads: 0 acted (dry run) · 3 duplicate (counted delivered) · 2 rejected or expired (never
|
|
9
|
+
// retried) · anything else is retried. A line per signal goes to --log (default autotrade/real/kit-cmd.log).
|
|
10
|
+
|
|
11
|
+
import { appendFileSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { SeenStore, args, dextermHome, handle, heartbeat, readSecret, realDir } from './kit.mjs';
|
|
14
|
+
|
|
15
|
+
const a = args();
|
|
16
|
+
if (!a.account) { console.error('usage: signal-cmd-executor.mjs --account ID [--secret-file FILE] [--home DIR]'); process.exit(2); }
|
|
17
|
+
const home = a.home || dextermHome();
|
|
18
|
+
const logFile = a.log || join(realDir(home), 'kit-cmd.log');
|
|
19
|
+
const log = (s) => { try { appendFileSync(logFile, `[${new Date().toISOString()}] ${s}\n`); } catch { /* the exit code still answers */ } };
|
|
20
|
+
const name = String(a.name || 'kit-signal-cmd').slice(0, 32);
|
|
21
|
+
const body = readFileSync(0, 'utf8').trim();
|
|
22
|
+
const seen = new SeenStore(a.state || join(realDir(home), 'kit-cmd-seen.json'));
|
|
23
|
+
const c = handle(body, { accountId: a.account, secret: readSecret(a['secret-file']), requireHmac: !!a['require-hmac'], signature: process.env.DEXTERM_SIGNAL_SIGNATURE ?? null, seen, home, executor: name, log, now: Date.now() });
|
|
24
|
+
try { heartbeat(home, { executor: name, accountId: a.account, lastNonce: seen.maxNonce || null }); } catch { /* optional */ }
|
|
25
|
+
process.exit(c.verdict === 'act' ? 0 : c.verdict === 'duplicate' ? 3 : 2);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Recipe: a Telegram trading bot as your executor
|
|
2
|
+
|
|
3
|
+
Many people already trade memecoins through a Telegram trading bot. That bot holds the wallet and signs the swaps.
|
|
4
|
+
dexterm can drive the decision side, but it can never be the bot and never holds the bot's key.
|
|
5
|
+
|
|
6
|
+
## Why the Telegram copy alone is not enough
|
|
7
|
+
|
|
8
|
+
dexterm's Telegram copy of each signal goes to **your** chat, for you to read. A bot cannot read another bot's
|
|
9
|
+
messages, so your trading bot never sees that copy. Something on your side has to hand each signal to the bot.
|
|
10
|
+
|
|
11
|
+
## The shape that works
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
dexterm (real mode) --signal--> your small relay (one of the kit executors) --order--> your trading bot
|
|
15
|
+
<--receipt-- <--fill--
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
1. Start from one of the kit executors, for example `webhook-executor.mjs`. It already verifies the HMAC, dedupes
|
|
19
|
+
by nonce, honours the TTL, writes receipts and sends heartbeats.
|
|
20
|
+
2. Replace `act()` in `kit.mjs` with the call your bot offers to accounts that automate it. Depending on the bot
|
|
21
|
+
that is an HTTP API, a CLI, or a message your own user account sends to it through a Telegram client library.
|
|
22
|
+
Use only what the bot documents and allows. That code, its credentials and the wallet are yours; keep them out
|
|
23
|
+
of dexterm's folders.
|
|
24
|
+
3. When the bot answers, write the receipt with the real fill: `status: "executed"` plus `priceUsd`, `sizeSol`,
|
|
25
|
+
`tokens` and `txSig`, or `rejected` / `failed` with a reason. dexterm then shows how far the real fills were
|
|
26
|
+
from paper.
|
|
27
|
+
4. Keep the relay's heartbeat running. If the relay or the bot goes quiet longer than your `executorSilenceMin`,
|
|
28
|
+
dexterm pauses new buy signals and tells you.
|
|
29
|
+
|
|
30
|
+
## Rules the relay must keep (the kit does)
|
|
31
|
+
|
|
32
|
+
- It acts only on signals for its own `accountId`.
|
|
33
|
+
- It never acts after `expiresAt`, and it never retries a stale order on its own.
|
|
34
|
+
- It dedupes by `nonce` and `id`, because the same signal can arrive twice.
|
|
35
|
+
- It treats `type: "test"` as a test: there is never an order.
|
|
36
|
+
- On `type: "halted"`, it may cancel the `outstanding` nonces that are still open on its side. `open` lists what
|
|
37
|
+
dexterm still counts as held.
|
|
38
|
+
- It sizes from its own wallet. `sizeSol` is what dexterm's limits allowed, and `sellFraction` applies to what the
|
|
39
|
+
bot really holds.
|
|
40
|
+
|
|
41
|
+
## Before any real money
|
|
42
|
+
|
|
43
|
+
- Run the relay in dry run first. `dexterm autotrade signal test` must show every channel delivered and the relay's
|
|
44
|
+
receipt.
|
|
45
|
+
- Set your own limits (`dexterm autotrade limits set …`). The defaults are placeholders: 0.25 SOL per trade, 1 SOL
|
|
46
|
+
a day, 20 buys, 3 open, stop at -0.5 SOL a day.
|
|
47
|
+
- Keep `dexterm autotrade signal kill` at hand. It stops every signal except one final `halted`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dexterm executor reference kit: the WEBHOOK receiver (task-1654). DRY RUN: logs what it would do; no wallet code.
|
|
3
|
+
//
|
|
4
|
+
// node webhook-executor.mjs --account main [--port 8787] [--secret-file FILE] [--home DIR] [--heartbeat-sec 60]
|
|
5
|
+
//
|
|
6
|
+
// Then point dexterm at it: dexterm autotrade signal channels --webhook http://127.0.0.1:8787/signal
|
|
7
|
+
// and prove the wiring: dexterm autotrade signal test
|
|
8
|
+
//
|
|
9
|
+
// Answers: 200 acted (dry run) · 409 duplicate (dexterm counts it delivered) · 410 expired · 401 bad signature ·
|
|
10
|
+
// 400 anything else it will not act on. It binds 127.0.0.1 only. Ctrl-C stops it.
|
|
11
|
+
|
|
12
|
+
import { createServer } from 'node:http';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { SeenStore, args, dextermHome, handle, heartbeat, readSecret, realDir } from './kit.mjs';
|
|
15
|
+
|
|
16
|
+
const a = args();
|
|
17
|
+
if (!a.account) { console.error('usage: node webhook-executor.mjs --account ID [--port 8787] [--secret-file FILE] [--home DIR]'); process.exit(2); }
|
|
18
|
+
const home = a.home || dextermHome();
|
|
19
|
+
const secret = readSecret(a['secret-file']);
|
|
20
|
+
const seen = new SeenStore(a.state || join(realDir(home), 'kit-webhook-seen.json'));
|
|
21
|
+
const name = String(a.name || 'kit-webhook').slice(0, 32);
|
|
22
|
+
const log = (s) => console.log(`[${new Date().toISOString()}] ${s}`);
|
|
23
|
+
const o = { accountId: a.account, secret, requireHmac: !!a['require-hmac'], seen, home, executor: name, log };
|
|
24
|
+
|
|
25
|
+
const server = createServer((req, res) => {
|
|
26
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('POST only'); return; }
|
|
27
|
+
let body = '';
|
|
28
|
+
req.setEncoding('utf8');
|
|
29
|
+
req.on('data', (c) => { body += c; if (body.length > 64 * 1024) req.destroy(); });
|
|
30
|
+
req.on('end', () => {
|
|
31
|
+
const c = handle(body, { ...o, signature: req.headers['x-dexterm-signature'] ?? null, now: Date.now() });
|
|
32
|
+
const code = c.verdict === 'act' ? 200 : c.verdict === 'duplicate' ? 409 : c.verdict === 'expired' ? 410 : /^signature/.test(c.why ?? '') ? 401 : 400;
|
|
33
|
+
res.writeHead(code, { 'content-type': 'text/plain' });
|
|
34
|
+
res.end(c.verdict);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const beat = () => { try { heartbeat(home, { executor: name, accountId: a.account, lastNonce: seen.maxNonce || null }); } catch (e) { log(`heartbeat failed: ${e.message}`); } };
|
|
39
|
+
server.listen(Number(a.port ?? 8787), '127.0.0.1', () => {
|
|
40
|
+
const port = server.address().port;
|
|
41
|
+
log(`dexterm kit webhook executor (DRY RUN) listening on http://127.0.0.1:${port}/signal · account ${a.account} · HMAC ${secret ? 'checked' : 'NOT checked (no --secret-file)'}`);
|
|
42
|
+
beat();
|
|
43
|
+
});
|
|
44
|
+
const timer = setInterval(beat, Math.max(5, Number(a['heartbeat-sec'] ?? 60)) * 1000);
|
|
45
|
+
const stop = () => { clearInterval(timer); server.close(() => process.exit(0)); };
|
|
46
|
+
process.on('SIGINT', stop);
|
|
47
|
+
process.on('SIGTERM', stop);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { SKILL_MIN_N, wilson } from '../../packs/src/index.js';
|
|
20
20
|
import { EntryRules } from "./engine.js";
|
|
21
21
|
import { runAutotrade } from "./runner.js";
|
|
22
|
+
import { rugproofTapeVet } from "./rugproof.js";
|
|
22
23
|
import { validateStrategy, round } from "./strategy.js";
|
|
23
24
|
export const DATA_LIMITS = [
|
|
24
25
|
'Price path = recorder snapshots (~2-min cadence while listed, written on >= 5% moves or every 30 min), not candles or trades: intrabar spikes and dips are invisible, so stops and take-profits fill at the next recorded price.',
|
|
@@ -31,8 +32,12 @@ export const DATA_LIMITS = [
|
|
|
31
32
|
];
|
|
32
33
|
/** A closed trade at or below this is counted as a rug (the challenge's rugAtPct). */
|
|
33
34
|
export const RUG_PCT = -80;
|
|
34
|
-
export async function backtest(strategy, tape, sessions = null) {
|
|
35
|
-
|
|
35
|
+
export async function backtest(strategy, tape, sessions = null, opts = {}) {
|
|
36
|
+
// task-1748: a rug-proof strategy replays its tick gates + the pump.fun-mint proxy (the chain gates need live data).
|
|
37
|
+
const rp = strategy.entry.safety.rugproof;
|
|
38
|
+
const vet = opts.vet === null ? undefined : opts.vet ?? (rp ? rugproofTapeVet(rp) : undefined);
|
|
39
|
+
// task-1645: a strategy in mode real backtests as its paper twin (a backtest never emits a trade-fire signal).
|
|
40
|
+
const run = await runAutotrade({ strategy: strategy.mode === 'real' ? { ...strategy, mode: 'paper' } : strategy, tape, sessions, pacing: { mode: 'lockstep' }, ...(vet ? { vet } : {}) });
|
|
36
41
|
return { report: report(strategy, tape, run), run };
|
|
37
42
|
}
|
|
38
43
|
export function report(s, tape, run) {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// $DEXTERM_HOME/connectors.json (0600):
|
|
4
4
|
// { schema, telegram: { enabled, bot: {id, username}, secretStore, tiers, allow: [{senderId, chatId, tiers,
|
|
5
5
|
// pairedAt}], notify: {…per event…}, dailySummaryAt, quietHours, approvals, approvalExpirySec, repriceTolPct,
|
|
6
|
-
// showAbsoluteBook } }
|
|
6
|
+
// showAbsoluteBook }, signals: {…} }
|
|
7
|
+
// task-1645: `signals` is the real-mode trade-fire channel setup (signal-channels.ts parses it); it lives here so the
|
|
8
|
+
// Telegram copy and the executor channels are configured in one place. This module only carries it through.
|
|
7
9
|
// Only local commands write it (`dexterm telegram setup|pair|revoke|approvals|notify`). From chat, exactly one
|
|
8
10
|
// field can change: `approvals` from "off" to "on" (safer); everything else, and "off", is local only.
|
|
9
11
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
@@ -70,7 +72,7 @@ export function loadConnectors(home) {
|
|
|
70
72
|
repriceTolPct: Number.isFinite(tol) && tol > 0 ? Math.min(10, tol) : 3,
|
|
71
73
|
showAbsoluteBook: t.showAbsoluteBook === true,
|
|
72
74
|
};
|
|
73
|
-
return { ok: true, config: { schema: CONNECTORS_SCHEMA, telegram }, exists: true };
|
|
75
|
+
return { ok: true, config: { schema: CONNECTORS_SCHEMA, telegram, ...(d.signals !== undefined ? { signals: d.signals } : {}) }, exists: true };
|
|
74
76
|
}
|
|
75
77
|
export function saveConnectors(home, c) {
|
|
76
78
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
@@ -45,6 +45,7 @@ export class ControlRouter {
|
|
|
45
45
|
`open ${s.open} · candidates ${s.candidates} · day P/L ${pct(s.dayPnlPct)}`,
|
|
46
46
|
`model spend today $${s.spend.todayUsd.toFixed(2)}${Number.isFinite(s.spend.dayCapUsd) ? ` of $${s.spend.dayCapUsd}` : ''}${s.allowance?.fiveHour !== null && s.allowance?.fiveHour !== undefined ? ` · 5 h window ${Math.round(s.allowance.fiveHour * 100)}%` : ''}`,
|
|
47
47
|
`approvals: ${s.approvals}`,
|
|
48
|
+
...(s.real ? [`executor: ${inert(s.real.executor, 160)}`, `limits: ${inert(s.real.limits, 160)}`, ...(s.real.pause ? [`NEW BUYS PAUSED: ${inert(s.real.pause, 120)}`] : [])] : []),
|
|
48
49
|
],
|
|
49
50
|
};
|
|
50
51
|
}
|
|
@@ -49,6 +49,9 @@ export class NotifyBus {
|
|
|
49
49
|
warned = new Set();
|
|
50
50
|
summarySentFor = '';
|
|
51
51
|
dayStart;
|
|
52
|
+
/** task-1645: a run in mode real is LIVE in every message (its trades go to the user's executor as signals). */
|
|
53
|
+
realRun = false;
|
|
54
|
+
badge() { return this.realRun ? 'LIVE' : this.o.badge; }
|
|
52
55
|
constructor(o) {
|
|
53
56
|
this.o = o;
|
|
54
57
|
this.now = o.now ?? Date.now;
|
|
@@ -77,7 +80,7 @@ export class NotifyBus {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
emit(template, lines, urgent = false) {
|
|
80
|
-
this.post(template, { kind: 'text', badge: this.
|
|
83
|
+
this.post(template, { kind: 'text', badge: this.badge(), lines }, urgent);
|
|
81
84
|
}
|
|
82
85
|
post(template, msg, urgent) {
|
|
83
86
|
const now = this.now();
|
|
@@ -95,7 +98,7 @@ export class NotifyBus {
|
|
|
95
98
|
ofBook(usd) {
|
|
96
99
|
const eq = this.state?.book.equityUsd ?? 0;
|
|
97
100
|
const share = eq > 0 ? `${((usd / eq) * 100).toFixed(1)}% of book` : '% of book unknown';
|
|
98
|
-
return this.o.config().showAbsoluteBook && this.
|
|
101
|
+
return this.o.config().showAbsoluteBook && this.badge() === 'PAPER' && eq > 0 ? `${share}, book ${usdShort(eq)}` : share;
|
|
99
102
|
}
|
|
100
103
|
sym(mint, fallback) { return `"${inert(this.symbols.get(mint) ?? fallback ?? '?', 20)}" ${shortMint(mint)}`; }
|
|
101
104
|
handle(e) {
|
|
@@ -105,8 +108,9 @@ export class NotifyBus {
|
|
|
105
108
|
switch (e.kind) {
|
|
106
109
|
case 'run_start': {
|
|
107
110
|
this.highRisk = e.highRisk === true;
|
|
111
|
+
this.realRun = e.mode === 'real';
|
|
108
112
|
this.strategyLine = `${this.highRisk ? '⚠ HIGH RISK · ' : ''}${inert(e.strategy, 32)} v${inert(e.version, 12)} (${String(e.strategyHash ?? '').slice(0, 8)})`;
|
|
109
|
-
this.emit('run_start', [`RUNNER STARTED · ${this.strategyLine}`, ...(this.highRisk ? ['full-port strategy: one entry can be the whole stake, and one rug can zero it. Experimental; not financial advice.'] : []), `entries: ${e.decide === 'agent+approve' || e.approvals === 'required' ? 'each one waits for your tap' : String(e.decide) === 'auto' ? 'mechanical' : 'your agent decides'} · /status /pause /kill /help`], true);
|
|
113
|
+
this.emit('run_start', [`RUNNER STARTED · ${this.strategyLine}`, ...(this.highRisk ? ['full-port strategy: one entry can be the whole stake, and one rug can zero it. Experimental; not financial advice.'] : []), ...(this.realRun ? ['REAL MODE: every trade below also goes to YOUR executor as a LIVE TRADE FIRE signal (dexterm holds no key). Experimental; not financial advice.'] : []), `entries: ${e.decide === 'agent+approve' || e.approvals === 'required' ? 'each one waits for your tap' : String(e.decide) === 'auto' ? 'mechanical' : 'your agent decides'} · /status /pause /kill /help`], true);
|
|
110
114
|
return;
|
|
111
115
|
}
|
|
112
116
|
case 'candidate': {
|
|
@@ -171,13 +175,19 @@ export class NotifyBus {
|
|
|
171
175
|
return;
|
|
172
176
|
const caption = `CLOSED · "${inert(t.symbol, 20)}" ${shortMint(t.mint)} · ${pct(t.pnlPct)} (${t.pnlUsd >= 0 ? '+' : ''}${usdShort(t.pnlUsd)}) by ${inert(t.exitReason, 40)} · held ${Math.max(1, Math.round((t.closedAt - t.openedAt) / 60_000))} min`;
|
|
173
177
|
const card = this.o.card;
|
|
178
|
+
// task-1645: a real run's close is the monitor's modelled view, not your executor's fills: text only, no
|
|
179
|
+
// shareable card (the receipts say what really filled).
|
|
180
|
+
if (this.realRun) {
|
|
181
|
+
this.emit('closed', [`${caption} · modelled; your executor's fills may differ (dexterm autotrade receipt list)`]);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
174
184
|
if (!card) {
|
|
175
185
|
this.emit('closed', [caption]);
|
|
176
186
|
return;
|
|
177
187
|
}
|
|
178
188
|
void card(t).then((png) => {
|
|
179
189
|
if (png)
|
|
180
|
-
this.post('closed_card', { kind: 'card', badge: this.
|
|
190
|
+
this.post('closed_card', { kind: 'card', badge: this.badge(), png, caption }, false);
|
|
181
191
|
else
|
|
182
192
|
this.emit('closed', [caption]);
|
|
183
193
|
}, () => this.emit('closed', [caption]));
|
|
@@ -253,7 +263,7 @@ export class NotifyBus {
|
|
|
253
263
|
this.sentAt.shift();
|
|
254
264
|
if (this.digest.length && !quiet && this.sentAt.length < DIGEST_ABOVE_PER_MIN / 2) {
|
|
255
265
|
const lines = this.digest.splice(0);
|
|
256
|
-
this.post('digest', { kind: 'text', badge: this.
|
|
266
|
+
this.post('digest', { kind: 'text', badge: this.badge(), lines: [`DIGEST · ${lines.length} update${lines.length === 1 ? '' : 's'}`, ...lines.slice(-25)] }, true);
|
|
257
267
|
}
|
|
258
268
|
this.maybeSummary(now);
|
|
259
269
|
await this.queue;
|
|
@@ -270,7 +280,7 @@ export class NotifyBus {
|
|
|
270
280
|
if (this.summarySentFor === day)
|
|
271
281
|
return;
|
|
272
282
|
this.summarySentFor = day;
|
|
273
|
-
this.post('daily', { kind: 'text', badge: this.
|
|
283
|
+
this.post('daily', { kind: 'text', badge: this.badge(), lines: this.summaryLines() }, true);
|
|
274
284
|
}
|
|
275
285
|
summaryLines() {
|
|
276
286
|
const st = this.state;
|
|
@@ -14,12 +14,18 @@ import { join, resolve } from 'node:path';
|
|
|
14
14
|
export const SECRET_SERVICE = 'xyz.dexterm.telegram';
|
|
15
15
|
export const accountFor = (home) => `bot-token-${createHash('sha256').update(resolve(home)).digest('hex').slice(0, 12)}`;
|
|
16
16
|
const has = (bin) => spawnSync('/usr/bin/env', ['which', bin], { stdio: 'ignore' }).status === 0;
|
|
17
|
+
export const TELEGRAM_TOKEN_ITEM = { service: SECRET_SERVICE, label: 'dexterm telegram bot', file: 'telegram-bot.token', account: accountFor };
|
|
18
|
+
export const SIGNAL_HMAC_ITEM = {
|
|
19
|
+
service: 'xyz.dexterm.signal', label: 'dexterm trade-fire webhook secret', file: 'signal-webhook.secret',
|
|
20
|
+
account: (home) => `signal-hmac-${createHash('sha256').update(resolve(home)).digest('hex').slice(0, 12)}`,
|
|
21
|
+
};
|
|
17
22
|
class KeychainStore {
|
|
18
23
|
kind = 'keychain';
|
|
19
24
|
account;
|
|
20
|
-
|
|
25
|
+
item;
|
|
26
|
+
constructor(account, item = TELEGRAM_TOKEN_ITEM) { this.account = account; this.item = item; }
|
|
21
27
|
get() {
|
|
22
|
-
const r = spawnSync('security', ['find-generic-password', '-a', this.account, '-s',
|
|
28
|
+
const r = spawnSync('security', ['find-generic-password', '-a', this.account, '-s', this.item.service, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
23
29
|
const v = r.status === 0 ? r.stdout.trim() : '';
|
|
24
30
|
return v || null;
|
|
25
31
|
}
|
|
@@ -27,32 +33,33 @@ class KeychainStore {
|
|
|
27
33
|
if (!/^[A-Za-z0-9:_-]+$/.test(value))
|
|
28
34
|
throw new Error('refusing to store a value with characters a bot token never has');
|
|
29
35
|
// The add command (with the value) goes through stdin; argv only says "-i".
|
|
30
|
-
const r = spawnSync('security', ['-i'], { input: `add-generic-password -U -a ${this.account} -s ${
|
|
36
|
+
const r = spawnSync('security', ['-i'], { input: `add-generic-password -U -a ${this.account} -s ${this.item.service} -l "${this.item.label}" -w "${value}"\n`, encoding: 'utf8', stdio: ['pipe', 'ignore', 'pipe'] });
|
|
31
37
|
if (r.status !== 0 || this.get() !== value)
|
|
32
38
|
throw new Error('the macOS keychain did not store the token');
|
|
33
39
|
}
|
|
34
|
-
remove() { spawnSync('security', ['delete-generic-password', '-a', this.account, '-s',
|
|
40
|
+
remove() { spawnSync('security', ['delete-generic-password', '-a', this.account, '-s', this.item.service], { stdio: 'ignore' }); }
|
|
35
41
|
}
|
|
36
42
|
class LibsecretStore {
|
|
37
43
|
kind = 'libsecret';
|
|
38
44
|
account;
|
|
39
|
-
|
|
45
|
+
item;
|
|
46
|
+
constructor(account, item = TELEGRAM_TOKEN_ITEM) { this.account = account; this.item = item; }
|
|
40
47
|
get() {
|
|
41
|
-
const r = spawnSync('secret-tool', ['lookup', 'service',
|
|
48
|
+
const r = spawnSync('secret-tool', ['lookup', 'service', this.item.service, 'account', this.account], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
42
49
|
const v = r.status === 0 ? r.stdout.trim() : '';
|
|
43
50
|
return v || null;
|
|
44
51
|
}
|
|
45
52
|
set(value) {
|
|
46
|
-
const r = spawnSync('secret-tool', ['store',
|
|
53
|
+
const r = spawnSync('secret-tool', ['store', `--label=${this.item.label}`, 'service', this.item.service, 'account', this.account], { input: value, encoding: 'utf8', stdio: ['pipe', 'ignore', 'pipe'] });
|
|
47
54
|
if (r.status !== 0 || this.get() !== value)
|
|
48
55
|
throw new Error('libsecret did not store the token');
|
|
49
56
|
}
|
|
50
|
-
remove() { spawnSync('secret-tool', ['clear', 'service',
|
|
57
|
+
remove() { spawnSync('secret-tool', ['clear', 'service', this.item.service, 'account', this.account], { stdio: 'ignore' }); }
|
|
51
58
|
}
|
|
52
59
|
class FileStore {
|
|
53
60
|
kind = 'file';
|
|
54
61
|
path;
|
|
55
|
-
constructor(home) { this.path = join(home, 'secrets',
|
|
62
|
+
constructor(home, item = TELEGRAM_TOKEN_ITEM) { this.path = join(home, 'secrets', item.file); }
|
|
56
63
|
get() {
|
|
57
64
|
if (!existsSync(this.path))
|
|
58
65
|
return null;
|
|
@@ -77,23 +84,23 @@ export class MemoryStore {
|
|
|
77
84
|
remove() { this.v = null; }
|
|
78
85
|
}
|
|
79
86
|
/** The store a kind names (the one setup recorded), for this home. */
|
|
80
|
-
export function secretStore(kind, home) {
|
|
87
|
+
export function secretStore(kind, home, item = TELEGRAM_TOKEN_ITEM) {
|
|
81
88
|
if (kind === 'keychain')
|
|
82
|
-
return new KeychainStore(
|
|
89
|
+
return new KeychainStore(item.account(home), item);
|
|
83
90
|
if (kind === 'libsecret')
|
|
84
|
-
return new LibsecretStore(
|
|
91
|
+
return new LibsecretStore(item.account(home), item);
|
|
85
92
|
if (kind === 'memory')
|
|
86
93
|
return new MemoryStore();
|
|
87
|
-
return new FileStore(home);
|
|
94
|
+
return new FileStore(home, item);
|
|
88
95
|
}
|
|
89
96
|
/** The best store on this machine: DEXTERM_SECRET_STORE wins, then the OS store, then the 0600 file. */
|
|
90
|
-
export function chooseSecretStore(home, env = process.env, platform = process.platform) {
|
|
97
|
+
export function chooseSecretStore(home, env = process.env, platform = process.platform, item = TELEGRAM_TOKEN_ITEM) {
|
|
91
98
|
const want = env.DEXTERM_SECRET_STORE;
|
|
92
99
|
if (want === 'file' || want === 'keychain' || want === 'libsecret')
|
|
93
|
-
return secretStore(want, home);
|
|
100
|
+
return secretStore(want, home, item);
|
|
94
101
|
if (platform === 'darwin' && has('security'))
|
|
95
|
-
return secretStore('keychain', home);
|
|
102
|
+
return secretStore('keychain', home, item);
|
|
96
103
|
if (platform === 'linux' && has('secret-tool'))
|
|
97
|
-
return secretStore('libsecret', home);
|
|
98
|
-
return secretStore('file', home);
|
|
104
|
+
return secretStore('libsecret', home, item);
|
|
105
|
+
return secretStore('file', home, item);
|
|
99
106
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { riskDelta } from '../../../mcp/src/index.js';
|
|
13
13
|
import { StrategyStore } from "../store.js";
|
|
14
14
|
import { requestControl } from "../safety.js";
|
|
15
|
+
import { healthLine } from "../executor-health.js";
|
|
16
|
+
import { limitsSummary } from "../limits.js";
|
|
15
17
|
import { badgeOf } from "../execution-port.js";
|
|
16
18
|
import { ApprovalBroker, CallbackStore, callbackDbPath } from "./approvals.js";
|
|
17
19
|
import { ChatAudit } from "./audit.js";
|
|
@@ -64,6 +66,7 @@ export class HostControlPort {
|
|
|
64
66
|
runId, state: st.state, haltReason: st.haltReason, strategy: st.strategy, open: st.positions.length, candidates: st.candidates.length,
|
|
65
67
|
dayPnlPct: start > 0 ? (st.book.realizedTodayUsd / start) * 100 : null, spend: { todayUsd: st.spend.todayUsd, dayCapUsd: st.spend.dayCapUsd }, allowance: st.spend.allowance,
|
|
66
68
|
approvals: s?.entry.decide === 'agent+approve' ? 'required by the strategy' : cfg.approvals,
|
|
69
|
+
...(st.real?.executor ? { real: { executor: healthLine(st.real.executor), limits: limitsSummary(st.real.limits ?? {}), pause: st.real.risk?.buyPause ?? null } } : {}),
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
positions() {
|
|
@@ -225,6 +225,19 @@ export class SimBook {
|
|
|
225
225
|
this.triggers.set(id, trigger);
|
|
226
226
|
return p;
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* 0.2.0 card B (service-recover.ts): take over a position a crashed run left open. Its open cost leaves this
|
|
230
|
+
* book's cash (it is exposure here now); the coin counts as entered, so the per-coin rules see it.
|
|
231
|
+
*/
|
|
232
|
+
adopt(p) {
|
|
233
|
+
if (this.open.has(p.mint))
|
|
234
|
+
return false;
|
|
235
|
+
this.state.cashUsd -= Math.max(0, p.costUsd - p.realizedUsd);
|
|
236
|
+
(this.state.perCoin[p.mint] ??= { entries: 0, lastExit: 0 }).entries++;
|
|
237
|
+
this.open.set(p.mint, p);
|
|
238
|
+
this.triggers.set(p.id, ['resumed']);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
228
241
|
addTranche(p, fill) {
|
|
229
242
|
this.state.cashUsd -= fill.usd;
|
|
230
243
|
const tokens = p.tokens + fill.tokens;
|
|
@@ -289,8 +302,9 @@ export function clampEntry(book, k, req, killSwitch = false) {
|
|
|
289
302
|
const st = book.state;
|
|
290
303
|
const clamps = [];
|
|
291
304
|
const no = (refused) => ({ ok: false, refused, clamps });
|
|
292
|
-
// task-1571: paper only (dexterm holds no key and places no live order).
|
|
293
|
-
|
|
305
|
+
// task-1571: paper only (dexterm holds no key and places no live order). task-1645: real is the same paper book; its
|
|
306
|
+
// fills become trade-fire signals for the user's own executor (the runner admits it only through the real-mode hook).
|
|
307
|
+
if (s.mode !== 'paper' && s.mode !== 'real')
|
|
294
308
|
return no('mode-not-paper');
|
|
295
309
|
if (killSwitch)
|
|
296
310
|
return no('kill-switch');
|