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,330 @@
|
|
|
1
|
+
// dexterm autotrade — trade-fire receipts, drift and the read-only wallet reconcile (task-1645, 0.2.0 card A).
|
|
2
|
+
//
|
|
3
|
+
// The user's executor reports what it did with a signal, as DATA only: `dexterm autotrade receipt …`, or by
|
|
4
|
+
// appending a `dexterm-trade-receipt/1` line to $DEXTERM_HOME/autotrade/real/receipts.jsonl itself.
|
|
5
|
+
// status executed | rejected | failed, optional fill {priceUsd, sizeSol, tokens, txSig}, a short reason
|
|
6
|
+
// A receipt never makes dexterm do anything: it only moves a real-mode position from `unconfirmed` to
|
|
7
|
+
// executed / rejected / failed in the ledger, the Q screen and `autotrade audit`, and measures the drift between
|
|
8
|
+
// what was signalled and what was filled. Exits keep firing on the monitor's own (paper-model) view regardless.
|
|
9
|
+
//
|
|
10
|
+
// The wallet reconcile reads a PUBLIC address the user supplies (SOL balance and token holdings, two read-only RPC
|
|
11
|
+
// methods on an allowlist) and lists the mints dexterm thinks are held that the wallet does not show, and back.
|
|
12
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { realDir } from "./outbox.js";
|
|
15
|
+
import { ACCOUNT_ID_RE } from "./signal.js";
|
|
16
|
+
export const RECEIPT_SCHEMA = 'dexterm-trade-receipt/1';
|
|
17
|
+
export const RECEIPT_STATUSES = ['executed', 'rejected', 'failed'];
|
|
18
|
+
export const receiptsPath = (home) => join(realDir(home), 'receipts.jsonl');
|
|
19
|
+
const TX_SIG = /^[1-9A-HJ-NP-Za-km-z]{64,90}$/;
|
|
20
|
+
const HEX32 = /^[0-9a-f]{32}$/;
|
|
21
|
+
const pos = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : null);
|
|
22
|
+
/** Free text from an executor is data: printable ASCII only, 120 chars. */
|
|
23
|
+
const text = (v, max) => { if (typeof v !== 'string')
|
|
24
|
+
return null; const t = v.replace(/[^\x20-\x7e]/g, '').trim().slice(0, max); return t || null; };
|
|
25
|
+
export function validateReceipt(x) {
|
|
26
|
+
const r = x;
|
|
27
|
+
if (!r || typeof r !== 'object')
|
|
28
|
+
return { ok: false, error: 'not an object' };
|
|
29
|
+
if (r.schema !== RECEIPT_SCHEMA)
|
|
30
|
+
return { ok: false, error: 'schema' };
|
|
31
|
+
if (typeof r.signalId !== 'string' || !HEX32.test(r.signalId))
|
|
32
|
+
return { ok: false, error: 'signalId' };
|
|
33
|
+
if (!Number.isSafeInteger(r.nonce) || r.nonce < 1)
|
|
34
|
+
return { ok: false, error: 'nonce' };
|
|
35
|
+
if (typeof r.accountId !== 'string' || !ACCOUNT_ID_RE.test(r.accountId))
|
|
36
|
+
return { ok: false, error: 'accountId' };
|
|
37
|
+
if (!RECEIPT_STATUSES.includes(r.status))
|
|
38
|
+
return { ok: false, error: 'status' };
|
|
39
|
+
if (typeof r.at !== 'number' || !Number.isFinite(r.at))
|
|
40
|
+
return { ok: false, error: 'at' };
|
|
41
|
+
let fill = null;
|
|
42
|
+
if (r.fill !== undefined && r.fill !== null) {
|
|
43
|
+
const f = r.fill;
|
|
44
|
+
if (typeof f !== 'object')
|
|
45
|
+
return { ok: false, error: 'fill' };
|
|
46
|
+
if (f.txSig !== undefined && f.txSig !== null && (typeof f.txSig !== 'string' || !TX_SIG.test(f.txSig)))
|
|
47
|
+
return { ok: false, error: 'fill.txSig is not a base58 signature' };
|
|
48
|
+
for (const k of ['priceUsd', 'sizeSol', 'tokens'])
|
|
49
|
+
if (f[k] !== undefined && f[k] !== null && pos(f[k]) === null)
|
|
50
|
+
return { ok: false, error: `fill.${k}` };
|
|
51
|
+
fill = { priceUsd: pos(f.priceUsd), sizeSol: pos(f.sizeSol), tokens: pos(f.tokens), txSig: typeof f.txSig === 'string' ? f.txSig : null };
|
|
52
|
+
}
|
|
53
|
+
if (r.status !== 'executed' && fill && (fill.sizeSol || fill.tokens))
|
|
54
|
+
return { ok: false, error: 'only an executed receipt carries a fill size' };
|
|
55
|
+
return { ok: true, receipt: { schema: RECEIPT_SCHEMA, signalId: r.signalId, nonce: r.nonce, accountId: r.accountId, status: r.status, at: r.at, fill, reason: text(r.reason, 120), executor: text(r.executor, 32) } };
|
|
56
|
+
}
|
|
57
|
+
export function appendReceipt(home, r) {
|
|
58
|
+
const v = validateReceipt(r);
|
|
59
|
+
if (!v.ok)
|
|
60
|
+
throw new Error(`receipt: ${v.error}`);
|
|
61
|
+
mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
|
|
62
|
+
const fd = openSync(receiptsPath(home), 'a', 0o600);
|
|
63
|
+
try {
|
|
64
|
+
writeSync(fd, `${JSON.stringify(v.receipt)}\n`);
|
|
65
|
+
fsyncSync(fd);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
closeSync(fd);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Why a receipt does not belong to a known signal (or null): unknown nonce, another account, an id mismatch. */
|
|
72
|
+
export function receiptMismatch(r, s) {
|
|
73
|
+
if (!s)
|
|
74
|
+
return 'unknown-signal';
|
|
75
|
+
if (s.id !== r.signalId)
|
|
76
|
+
return 'signal-id-mismatch';
|
|
77
|
+
if (s.accountId !== r.accountId)
|
|
78
|
+
return 'wrong-account';
|
|
79
|
+
// task-1654: a receipt for a `signal test` proves the round trip (signal test reports it); it is not an orphan.
|
|
80
|
+
if (s.type === 'test')
|
|
81
|
+
return 'test-signal';
|
|
82
|
+
if (s.type !== 'trade')
|
|
83
|
+
return 'not-a-trade-signal';
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reads receipts.jsonl incrementally (byte offset). Every valid receipt for a known signal is returned once; the
|
|
88
|
+
* rest are counted (`invalid`, `orphan`) and never acted on. A partial last line waits for its newline.
|
|
89
|
+
*/
|
|
90
|
+
export class ReceiptsTail {
|
|
91
|
+
offset = 0;
|
|
92
|
+
carry = '';
|
|
93
|
+
invalid = 0;
|
|
94
|
+
orphan = 0;
|
|
95
|
+
path;
|
|
96
|
+
lookup;
|
|
97
|
+
constructor(home, lookup) { this.path = receiptsPath(home); this.lookup = lookup; }
|
|
98
|
+
poll() {
|
|
99
|
+
if (!existsSync(this.path))
|
|
100
|
+
return [];
|
|
101
|
+
const size = statSync(this.path).size;
|
|
102
|
+
if (size < this.offset) {
|
|
103
|
+
this.offset = 0;
|
|
104
|
+
this.carry = '';
|
|
105
|
+
} // truncated: read again
|
|
106
|
+
if (size === this.offset)
|
|
107
|
+
return [];
|
|
108
|
+
const fd = openSync(this.path, 'r');
|
|
109
|
+
const buf = Buffer.alloc(Math.min(size - this.offset, 4 << 20));
|
|
110
|
+
let n = 0;
|
|
111
|
+
try {
|
|
112
|
+
n = readSync(fd, buf, 0, buf.length, this.offset);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
closeSync(fd);
|
|
116
|
+
}
|
|
117
|
+
this.offset += n;
|
|
118
|
+
const chunk = this.carry + buf.subarray(0, n).toString('utf8');
|
|
119
|
+
const lines = chunk.split('\n');
|
|
120
|
+
this.carry = lines.pop() ?? '';
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const l of lines) {
|
|
123
|
+
if (!l.trim())
|
|
124
|
+
continue;
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = JSON.parse(l);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
this.invalid++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const v = validateReceipt(raw);
|
|
134
|
+
if (!v.ok) {
|
|
135
|
+
this.invalid++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const mm = receiptMismatch(v.receipt, this.lookup(v.receipt.nonce));
|
|
139
|
+
if (mm === 'test-signal')
|
|
140
|
+
continue;
|
|
141
|
+
if (mm) {
|
|
142
|
+
this.orphan++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
out.push(v.receipt);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function readReceipts(home) {
|
|
151
|
+
const p = receiptsPath(home);
|
|
152
|
+
if (!existsSync(p))
|
|
153
|
+
return { receipts: [], invalid: 0 };
|
|
154
|
+
const out = [];
|
|
155
|
+
let invalid = 0;
|
|
156
|
+
for (const l of readFileSync(p, 'utf8').split('\n')) {
|
|
157
|
+
if (!l.trim())
|
|
158
|
+
continue;
|
|
159
|
+
try {
|
|
160
|
+
const v = validateReceipt(JSON.parse(l));
|
|
161
|
+
if (v.ok)
|
|
162
|
+
out.push(v.receipt);
|
|
163
|
+
else
|
|
164
|
+
invalid++;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
invalid++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { receipts: out, invalid };
|
|
171
|
+
}
|
|
172
|
+
const r1 = (v) => Math.round(v * 10) / 10;
|
|
173
|
+
/** Signals vs receipts: one row per trade signal. */
|
|
174
|
+
export function driftReport(signals, receipts, now, tolPct = 5) {
|
|
175
|
+
const trades = signals.filter((s) => s.type === 'trade');
|
|
176
|
+
const byNonce = new Map();
|
|
177
|
+
let orphanReceipts = 0;
|
|
178
|
+
const known = new Map(trades.map((s) => [s.nonce, s]));
|
|
179
|
+
const tests = new Set(signals.filter((s) => s.type === 'test').map((s) => s.nonce));
|
|
180
|
+
for (const r of receipts) {
|
|
181
|
+
if (tests.has(r.nonce))
|
|
182
|
+
continue;
|
|
183
|
+
if (receiptMismatch(r, known.get(r.nonce))) {
|
|
184
|
+
orphanReceipts++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const l = byNonce.get(r.nonce) ?? [];
|
|
188
|
+
l.push(r);
|
|
189
|
+
byNonce.set(r.nonce, l);
|
|
190
|
+
}
|
|
191
|
+
const counts = { unconfirmed: 0, executed: 0, rejected: 0, failed: 0, 'expired-unconfirmed': 0 };
|
|
192
|
+
let sizeDrift = 0;
|
|
193
|
+
const rows = trades.map((s) => {
|
|
194
|
+
const rs = (byNonce.get(s.nonce) ?? []).sort((a, b) => a.at - b.at);
|
|
195
|
+
const last = rs[rs.length - 1];
|
|
196
|
+
const state = last ? last.status : now > s.expiresAt ? 'expired-unconfirmed' : 'unconfirmed';
|
|
197
|
+
counts[state]++;
|
|
198
|
+
const filled = last?.status === 'executed' ? last.fill?.sizeSol ?? null : null;
|
|
199
|
+
const sizeDriftPct = filled !== null && s.sizeSol > 0 ? r1(((filled - s.sizeSol) / s.sizeSol) * 100) : null;
|
|
200
|
+
const px = last?.status === 'executed' ? last.fill?.priceUsd ?? null : null;
|
|
201
|
+
const priceDriftPct = px !== null && s.price.usd ? r1(((px - s.price.usd) / s.price.usd) * 100) : null;
|
|
202
|
+
if (sizeDriftPct !== null && Math.abs(sizeDriftPct) > tolPct)
|
|
203
|
+
sizeDrift++;
|
|
204
|
+
return {
|
|
205
|
+
nonce: s.nonce, id: s.id, runId: s.runId, side: s.side, mint: s.mint, symbol: s.symbol, issuedAt: s.issuedAt, state, signalSol: s.sizeSol, filledSol: filled, sizeDriftPct, priceDriftPct,
|
|
206
|
+
txSig: last?.fill?.txSig ?? null, conflict: new Set(rs.map((r) => r.status)).size > 1, reason: last?.reason ?? null,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
return { rows, counts, unconfirmed: counts.unconfirmed + counts['expired-unconfirmed'], sizeDrift, orphanReceipts };
|
|
210
|
+
}
|
|
211
|
+
const r2 = (v) => Math.round(v * 100) / 100;
|
|
212
|
+
/** One receipt against its trade signal (null for anything that is not a known trade signal). */
|
|
213
|
+
export function divergenceOf(s, r) {
|
|
214
|
+
if (!s || s.type !== 'trade' || receiptMismatch(r, s))
|
|
215
|
+
return null;
|
|
216
|
+
const f = r.status === 'executed' ? r.fill : null;
|
|
217
|
+
const px = f?.priceUsd ?? (f?.sizeSol && f.tokens ? (f.sizeSol * s.price.solUsd) / f.tokens : null);
|
|
218
|
+
const adverse = (ref) => (px && ref && ref > 0 ? r2(((s.side === 'buy' ? px - ref : ref - px) / ref) * 100) : null);
|
|
219
|
+
const modelTokens = s.model?.tokens ?? null;
|
|
220
|
+
const fillRatio = !f ? null : s.side === 'buy' ? (f.sizeSol && s.sizeSol > 0 ? Math.round((f.sizeSol / s.sizeSol) * 1000) / 1000 : null) : (f.tokens && modelTokens ? Math.round((f.tokens / modelTokens) * 1000) / 1000 : null);
|
|
221
|
+
return {
|
|
222
|
+
nonce: s.nonce, side: s.side, status: r.status, slippageVsPaperPct: adverse(s.model?.priceUsd ?? null), slippageVsQuotePct: adverse(s.price.usd),
|
|
223
|
+
latencyMs: Math.max(0, r.at - s.issuedAt), fillRatio, partial: fillRatio !== null && fillRatio < 0.95, reason: r.reason,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const quantile = (xs, q) => { if (!xs.length)
|
|
227
|
+
return null; const a = [...xs].sort((x, y) => x - y); return a[Math.min(a.length - 1, Math.floor(q * a.length))]; };
|
|
228
|
+
/** Every trade signal's newest receipt, summarized. */
|
|
229
|
+
export function divergenceReport(signals, receipts) {
|
|
230
|
+
const bySig = new Map(signals.filter((s) => s.type === 'trade').map((s) => [s.nonce, s]));
|
|
231
|
+
const newest = new Map();
|
|
232
|
+
for (const r of receipts) {
|
|
233
|
+
const s = bySig.get(r.nonce);
|
|
234
|
+
if (!s || receiptMismatch(r, s))
|
|
235
|
+
continue;
|
|
236
|
+
const p = newest.get(r.nonce);
|
|
237
|
+
if (!p || r.at >= p.at)
|
|
238
|
+
newest.set(r.nonce, r);
|
|
239
|
+
}
|
|
240
|
+
const rows = [...newest.values()].map((r) => divergenceOf(bySig.get(r.nonce), r)).filter((d) => !!d).sort((a, b) => a.nonce - b.nonce);
|
|
241
|
+
const ex = rows.filter((d) => d.status === 'executed');
|
|
242
|
+
const sl = ex.map((d) => d.slippageVsPaperPct ?? d.slippageVsQuotePct).filter((v) => v !== null);
|
|
243
|
+
const lat = rows.map((d) => d.latencyMs);
|
|
244
|
+
const reasons = {};
|
|
245
|
+
for (const d of rows)
|
|
246
|
+
if (d.status !== 'executed') {
|
|
247
|
+
const k = `${d.status}: ${(d.reason ?? 'no reason').slice(0, 40)}`;
|
|
248
|
+
reasons[k] = (reasons[k] ?? 0) + 1;
|
|
249
|
+
}
|
|
250
|
+
const n = ex.length;
|
|
251
|
+
return {
|
|
252
|
+
rows, trades: bySig.size, receipted: rows.length, executed: n, rejected: rows.filter((d) => d.status === 'rejected').length, failed: rows.filter((d) => d.status === 'failed').length, partial: rows.filter((d) => d.partial).length,
|
|
253
|
+
slippage: { n: sl.length, medianPct: quantile(sl, 0.5), p90Pct: quantile(sl, 0.9), meanPct: sl.length ? r2(sl.reduce((a, b) => a + b, 0) / sl.length) : null },
|
|
254
|
+
latency: { n: lat.length, medianMs: quantile(lat, 0.5), p90Ms: quantile(lat, 0.9) },
|
|
255
|
+
reasons,
|
|
256
|
+
note: n === 0 ? 'no executed receipt yet: nothing to compare paper with' : n < 30 ? `only ${n} executed fill(s): too few to calibrate paper fills with confidence` : `${n} executed fills`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function divergenceLines(d) {
|
|
260
|
+
const out = [`paper vs real: ${d.receipted}/${d.trades} trade signals have a receipt · executed ${d.executed} · rejected ${d.rejected} · failed ${d.failed} · partial ${d.partial}`];
|
|
261
|
+
if (d.slippage.n)
|
|
262
|
+
out.push(` slippage vs paper fill: median ${d.slippage.medianPct}% · p90 ${d.slippage.p90Pct}% · mean ${d.slippage.meanPct}% (positive = the real fill was worse than paper)`);
|
|
263
|
+
if (d.latency.n)
|
|
264
|
+
out.push(` latency signal -> receipt: median ${Math.round((d.latency.medianMs ?? 0) / 100) / 10} s · p90 ${Math.round((d.latency.p90Ms ?? 0) / 100) / 10} s`);
|
|
265
|
+
for (const [k, v] of Object.entries(d.reasons).slice(0, 5))
|
|
266
|
+
out.push(` ${v}× ${k}`);
|
|
267
|
+
out.push(` ${d.note}`);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
// ───────────────────────────── read-only wallet reconcile ─────────────────────────────
|
|
271
|
+
/** The only RPC methods the reconcile may call. Anything else is refused before it leaves the process. */
|
|
272
|
+
export const RECONCILE_RPC_METHODS = ['getBalance', 'getTokenAccountsByOwner'];
|
|
273
|
+
const TOKEN_PROGRAMS = ['TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'];
|
|
274
|
+
const PUBKEY = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
275
|
+
/** Read a public wallet (balance + token holdings) and compare with the mints the real-mode book holds / closed. */
|
|
276
|
+
export async function reconcileWallet(wallet, rpc, book) {
|
|
277
|
+
if (!PUBKEY.test(wallet))
|
|
278
|
+
throw new Error('reconcile: the wallet is a public base58 address (never a key)');
|
|
279
|
+
const call = (m, p) => { if (!RECONCILE_RPC_METHODS.includes(m))
|
|
280
|
+
return Promise.reject(new Error(`rpc method ${m} is not on the read-only allowlist`)); return rpc(m, p); };
|
|
281
|
+
const errors = [];
|
|
282
|
+
let sol = null;
|
|
283
|
+
try {
|
|
284
|
+
const b = (await call('getBalance', [wallet, { commitment: 'confirmed' }]));
|
|
285
|
+
sol = typeof b?.value === 'number' ? b.value / 1e9 : null;
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
errors.push(`getBalance: ${e.message.slice(0, 120)}`);
|
|
289
|
+
}
|
|
290
|
+
const holdings = {};
|
|
291
|
+
for (const programId of TOKEN_PROGRAMS) {
|
|
292
|
+
try {
|
|
293
|
+
const r = (await call('getTokenAccountsByOwner', [wallet, { programId }, { encoding: 'jsonParsed', commitment: 'confirmed' }]));
|
|
294
|
+
for (const a of r?.value ?? []) {
|
|
295
|
+
const info = a.account?.data?.parsed?.info;
|
|
296
|
+
if (!info?.mint || !PUBKEY.test(info.mint))
|
|
297
|
+
continue;
|
|
298
|
+
holdings[info.mint] = (holdings[info.mint] ?? 0) + (Number(info.tokenAmount?.uiAmount) || 0);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
errors.push(`getTokenAccountsByOwner: ${e.message.slice(0, 120)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const ok = errors.length === 0;
|
|
306
|
+
return {
|
|
307
|
+
wallet, sol, holdings,
|
|
308
|
+
missing: ok ? book.held.filter((m) => !(holdings[m] > 0)) : [],
|
|
309
|
+
stillHeld: ok ? book.closed.filter((m) => holdings[m] > 0 && !book.held.includes(m)) : [],
|
|
310
|
+
errors,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** From the trade signals alone (nonce order): the mints a real-mode book holds, and those it closed. */
|
|
314
|
+
export function heldFromSignals(signals) {
|
|
315
|
+
const held = new Set();
|
|
316
|
+
const closed = new Set();
|
|
317
|
+
for (const s of [...signals].sort((a, b) => a.nonce - b.nonce)) {
|
|
318
|
+
if (s.type !== 'trade')
|
|
319
|
+
continue;
|
|
320
|
+
if (s.side === 'buy') {
|
|
321
|
+
held.add(s.mint);
|
|
322
|
+
closed.delete(s.mint);
|
|
323
|
+
}
|
|
324
|
+
else if ((s.sellFraction ?? 1) >= 0.999) {
|
|
325
|
+
held.delete(s.mint);
|
|
326
|
+
closed.add(s.mint);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { held: [...held], closed: [...closed] };
|
|
330
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// dexterm autotrade — the daily / weekly digest (task-1654, 0.2.0 card E). Read-only over the run ledgers.
|
|
2
|
+
//
|
|
3
|
+
// Per strategy and mode (paper | real) over a period: closed trades, the win rate with its 95% interval (Wilson),
|
|
4
|
+
// net PnL, the max drawdown of the closed-trade curve, refusals, the limits that were hit, and for real mode the
|
|
5
|
+
// executor's health and the paper-vs-real fill divergence from receipts. Every digest ends with an honest sample
|
|
6
|
+
// note: a handful of trades says almost nothing, and the interval shows how little.
|
|
7
|
+
//
|
|
8
|
+
// `dexterm autotrade report` prints it (or --json, or --telegram through the user's own bot). A run with the
|
|
9
|
+
// Telegram bot on sends it by itself when the user turned a schedule on (`report --schedule daily|weekly|off`,
|
|
10
|
+
// saved in $DEXTERM_HOME/autotrade/digest.json; off by default).
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { readLedgerFile, runsDir, writeJsonAtomic } from "./ledger.js";
|
|
14
|
+
import { localDay } from "./chat/audit.js";
|
|
15
|
+
import { allOutboxSignals } from "./outbox.js";
|
|
16
|
+
import { divergenceReport, readReceipts } from "./receipts.js";
|
|
17
|
+
const r2 = (v) => Math.round(v * 100) / 100;
|
|
18
|
+
/** The 95% Wilson score interval of k wins in n, in %. */
|
|
19
|
+
export function wilson(k, n, z = 1.96) {
|
|
20
|
+
if (n <= 0)
|
|
21
|
+
return null;
|
|
22
|
+
const p = k / n;
|
|
23
|
+
const d = 1 + (z * z) / n;
|
|
24
|
+
const c = (p + (z * z) / (2 * n)) / d;
|
|
25
|
+
const h = (z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n))) / d;
|
|
26
|
+
return [Math.round(Math.max(0, c - h) * 1000) / 10, Math.round(Math.min(1, c + h) * 1000) / 10];
|
|
27
|
+
}
|
|
28
|
+
export function periodBounds(period, now) {
|
|
29
|
+
return { from: now - (period === 'day' ? 1 : 7) * 86_400_000, to: now };
|
|
30
|
+
}
|
|
31
|
+
/** The digest of every run ledger with events in [from, to]. */
|
|
32
|
+
export function buildDigest(home, o) {
|
|
33
|
+
const b = periodBounds(o.period, o.now);
|
|
34
|
+
const from = o.from ?? b.from;
|
|
35
|
+
const to = o.to ?? b.to;
|
|
36
|
+
const by = new Map();
|
|
37
|
+
const dir = runsDir(home);
|
|
38
|
+
const files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.jsonl')) : [];
|
|
39
|
+
let anyReal = false;
|
|
40
|
+
for (const f of files) {
|
|
41
|
+
let ev;
|
|
42
|
+
try {
|
|
43
|
+
ev = readLedgerFile(join(dir, f));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const start = ev.find((e) => e.kind === 'run_start');
|
|
49
|
+
if (!start || typeof start.strategy !== 'string')
|
|
50
|
+
continue;
|
|
51
|
+
if (o.strategyId && start.strategy !== o.strategyId)
|
|
52
|
+
continue;
|
|
53
|
+
const inPeriod = ev.filter((e) => e.t >= from && e.t <= to);
|
|
54
|
+
if (!inPeriod.length)
|
|
55
|
+
continue;
|
|
56
|
+
const mode = start.mode === 'real' || start.tag === 'real-signal' ? 'real' : 'paper';
|
|
57
|
+
if (mode === 'real')
|
|
58
|
+
anyReal = true;
|
|
59
|
+
const key = `${start.strategy}|${mode}`;
|
|
60
|
+
let d = by.get(key);
|
|
61
|
+
if (!d) {
|
|
62
|
+
d = { strategyId: start.strategy, mode, runs: 0, trades: 0, wins: 0, winRatePct: null, winRateCi: null, netUsd: 0, maxDrawdownUsd: 0, refusals: 0, limitsHit: {}, clamped: {}, receipts: 0, closes: [] };
|
|
63
|
+
by.set(key, d);
|
|
64
|
+
}
|
|
65
|
+
d.runs++;
|
|
66
|
+
for (const e of inPeriod) {
|
|
67
|
+
if (e.kind === 'trade_closed') {
|
|
68
|
+
const pnl = Number(e.trade?.pnlUsd);
|
|
69
|
+
if (Number.isFinite(pnl)) {
|
|
70
|
+
d.trades++;
|
|
71
|
+
if (pnl > 0)
|
|
72
|
+
d.wins++;
|
|
73
|
+
d.netUsd += pnl;
|
|
74
|
+
d.closes.push({ t: e.t, pnl });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (e.kind === 'refused') {
|
|
78
|
+
d.refusals++;
|
|
79
|
+
const rule = String(e.rule ?? '');
|
|
80
|
+
const m = /^(?:fill-)?(real-[a-z-]+)/.exec(rule);
|
|
81
|
+
if (m)
|
|
82
|
+
d.limitsHit[m[1]] = (d.limitsHit[m[1]] ?? 0) + 1;
|
|
83
|
+
}
|
|
84
|
+
else if (e.kind === 'fill' && Array.isArray(e.limitClamps)) {
|
|
85
|
+
for (const c of e.limitClamps)
|
|
86
|
+
d.clamped[c] = (d.clamped[c] ?? 0) + 1;
|
|
87
|
+
}
|
|
88
|
+
else if (e.kind === 'receipt')
|
|
89
|
+
d.receipts++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const strategies = [...by.values()].map(({ closes, ...d }) => {
|
|
93
|
+
closes.sort((a, b2) => a.t - b2.t);
|
|
94
|
+
let cum = 0;
|
|
95
|
+
let peak = 0;
|
|
96
|
+
let dd = 0;
|
|
97
|
+
for (const c of closes) {
|
|
98
|
+
cum += c.pnl;
|
|
99
|
+
peak = Math.max(peak, cum);
|
|
100
|
+
dd = Math.max(dd, peak - cum);
|
|
101
|
+
}
|
|
102
|
+
return { ...d, netUsd: r2(d.netUsd), maxDrawdownUsd: r2(dd), winRatePct: d.trades ? Math.round((d.wins / d.trades) * 1000) / 10 : null, winRateCi: wilson(d.wins, d.trades) };
|
|
103
|
+
}).sort((a, b2) => a.strategyId.localeCompare(b2.strategyId) || a.mode.localeCompare(b2.mode));
|
|
104
|
+
let divergence = null;
|
|
105
|
+
if (anyReal) {
|
|
106
|
+
const sig = allOutboxSignals(home).filter((s) => s.issuedAt >= from && s.issuedAt <= to);
|
|
107
|
+
const d = divergenceReport(sig, readReceipts(home).receipts);
|
|
108
|
+
const { rows: _rows, ...rest } = d;
|
|
109
|
+
divergence = rest;
|
|
110
|
+
}
|
|
111
|
+
const n = strategies.reduce((a, s) => a + s.trades, 0);
|
|
112
|
+
const note = n === 0 ? 'no closed trades in this period'
|
|
113
|
+
: n < 30 ? `${n} closed trade(s) in total: a small sample. The win-rate intervals show how wide the truth may be; read no trend into it.`
|
|
114
|
+
: `${n} closed trades: still check the interval before trusting a win rate, and past results do not predict future ones`;
|
|
115
|
+
return { period: o.period, from, to, strategies, divergence, executor: o.executor ?? null, note };
|
|
116
|
+
}
|
|
117
|
+
const usd = (v) => `${v < 0 ? '-' : '+'}$${Math.abs(v).toFixed(2)}`;
|
|
118
|
+
/** Plain lines (CLI and Telegram; no token text: strategy ids and engine numbers only). */
|
|
119
|
+
export function digestLines(d) {
|
|
120
|
+
const out = [`dexterm ${d.period === 'day' ? 'daily' : 'weekly'} digest · ${new Date(d.from).toISOString().slice(0, 16).replace('T', ' ')} to ${new Date(d.to).toISOString().slice(0, 16).replace('T', ' ')} UTC · not financial advice`];
|
|
121
|
+
if (!d.strategies.length)
|
|
122
|
+
out.push(' no runs in this period');
|
|
123
|
+
for (const s of d.strategies) {
|
|
124
|
+
out.push(`${s.mode === 'real' ? 'REAL' : 'paper'} ${s.strategyId} · ${s.runs} run(s) · ${s.trades} trade(s)${s.trades ? ` · win ${s.winRatePct}% (95% interval ${s.winRateCi[0]}-${s.winRateCi[1]}%)` : ''} · net ${usd(s.netUsd)} · max drawdown $${s.maxDrawdownUsd.toFixed(2)}`);
|
|
125
|
+
const hit = Object.entries(s.limitsHit).map(([k, v]) => `${k} ×${v}`).join(', ');
|
|
126
|
+
const cl = Object.entries(s.clamped).map(([k, v]) => `${k} ×${v}`).join(', ');
|
|
127
|
+
out.push(` refusals ${s.refusals}${hit ? ` · limits hit: ${hit}` : ''}${cl ? ` · clamped by: ${cl}` : ''}${s.mode === 'real' ? ` · receipts ${s.receipts}` : ''}`);
|
|
128
|
+
}
|
|
129
|
+
if (d.divergence) {
|
|
130
|
+
const v = d.divergence;
|
|
131
|
+
out.push(`paper vs real fills: ${v.receipted}/${v.trades} receipted · executed ${v.executed} · rejected ${v.rejected} · failed ${v.failed} · partial ${v.partial}${v.slippage.n ? ` · slippage vs paper median ${v.slippage.medianPct}% (p90 ${v.slippage.p90Pct}%)` : ''}${v.latency.n ? ` · latency median ${Math.round((v.latency.medianMs ?? 0) / 100) / 10} s` : ''}`);
|
|
132
|
+
out.push(` ${v.note}`);
|
|
133
|
+
}
|
|
134
|
+
if (d.executor)
|
|
135
|
+
out.push(`executor: ${d.executor.alive === false ? 'SILENT' : d.executor.alive ? 'alive' : d.executor.lastSeenAt ? 'seen' : 'never seen'}${d.executor.silentSec !== null ? ` · last heard ${Math.round(d.executor.silentSec / 60)} min ago` : ''}${d.executor.lastReceipt ? ` · last receipt #${d.executor.lastReceipt.nonce} ${d.executor.lastReceipt.status}` : ''}`);
|
|
136
|
+
out.push(d.note);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
// ───────────────────────────── the schedule (opt-in) ─────────────────────────────
|
|
140
|
+
export const DIGEST_SCHEMA = 'dexterm-digest/1';
|
|
141
|
+
export const digestStatePath = (home) => join(home, 'autotrade', 'digest.json');
|
|
142
|
+
export function loadDigestState(home) {
|
|
143
|
+
try {
|
|
144
|
+
const d = JSON.parse(readFileSync(digestStatePath(home), 'utf8'));
|
|
145
|
+
if (d.schema === DIGEST_SCHEMA && ['off', 'daily', 'weekly'].includes(d.schedule))
|
|
146
|
+
return d;
|
|
147
|
+
}
|
|
148
|
+
catch { /* none */ }
|
|
149
|
+
return { schema: DIGEST_SCHEMA, schedule: 'off', lastSentDay: null, lastSentAt: null };
|
|
150
|
+
}
|
|
151
|
+
export function saveDigestState(home, s) {
|
|
152
|
+
mkdirSync(join(home, 'autotrade'), { recursive: true, mode: 0o700 });
|
|
153
|
+
writeJsonAtomic(digestStatePath(home), s);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Is a scheduled digest due? Daily: once per local day, for the day before (first check after midnight). Weekly: on
|
|
157
|
+
* a Monday, for the week before. Never twice for the same day.
|
|
158
|
+
*/
|
|
159
|
+
export function digestDue(s, now) {
|
|
160
|
+
if (s.schedule === 'off')
|
|
161
|
+
return null;
|
|
162
|
+
const today = localDay(now);
|
|
163
|
+
if (s.lastSentDay === today)
|
|
164
|
+
return null;
|
|
165
|
+
if (s.schedule === 'weekly' && new Date(now).getDay() !== 1)
|
|
166
|
+
return null;
|
|
167
|
+
return s.schedule === 'daily' ? 'day' : 'week';
|
|
168
|
+
}
|