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,393 @@
|
|
|
1
|
+
// `dexterm autotrade limits …` (task-1654, 0.2.0 card E): every real-mode limit, user-set, per scope.
|
|
2
|
+
//
|
|
3
|
+
// show the effective limits (for a strategy / account / mode) and where each number comes from
|
|
4
|
+
// set name=value … in a scope (global | real | paper | strategy:<id> | account:<id>); lowering is immediate,
|
|
5
|
+
// raising shows the old -> new diff and needs the typed phrase on a real terminal
|
|
6
|
+
// diff a proposed file (--file) against the limits now, or limits.json against dexterm's last journaled save
|
|
7
|
+
// reset a scope (or one --key) back to the placeholder defaults (a raise if that loosens anything: typed)
|
|
8
|
+
// accept take a hand-edited limits.json (typed when it raises anything; a lower-only edit is accepted as is)
|
|
9
|
+
// breaker `breaker reset`: lift a tripped drawdown breaker (typed)
|
|
10
|
+
// explain what each limit does schema the JSON Schema of the file
|
|
11
|
+
//
|
|
12
|
+
// No MCP tool, model decision or chat command reaches the writing paths: the analyst can read and explain the limits
|
|
13
|
+
// and propose a diff card (MCP limits_propose), which only YOU can apply here.
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { dextermHome } from '../../../custom/src/index.js';
|
|
17
|
+
import { DEFAULT_LIMITS, LIMIT_FIELDS, LIMIT_KEYS, changeLine, diffLimits, effectiveLimits, emptyLimits, limitField, limitsLines, limitsPath, limitsSummary, loadArmings, loadLimits, loadRiskState, parseLimitAssignment, planLimitsSave, raises, resetBreaker, saveLimits, scopeRef, validateLimits, } from '../../../autotrade/src/index.js';
|
|
18
|
+
import { parseArgs } from "./strategies.js";
|
|
19
|
+
import { confirmTyped, isTty, typedLine } from "./signal.js";
|
|
20
|
+
export const LIMITS_USAGE = [
|
|
21
|
+
'dexterm autotrade limits show [--strategy ID] [--account ID] [--mode real|paper] [--all] [--json]',
|
|
22
|
+
'dexterm autotrade limits set [--scope global|real|paper|strategy:ID|account:ID] name=value … (value "off" turns a global limit off)',
|
|
23
|
+
'dexterm autotrade limits diff [--file FILE] [--json]',
|
|
24
|
+
'dexterm autotrade limits reset [--scope S] [--key NAME]',
|
|
25
|
+
'dexterm autotrade limits accept | breaker reset | explain [NAME] | schema',
|
|
26
|
+
].join('\n');
|
|
27
|
+
const out = (io, s) => { io.stdout.write(`${s}\n`); };
|
|
28
|
+
const err = (io, s) => { io.stderr.write(`dexterm autotrade limits: ${s}\n`); };
|
|
29
|
+
export const LIMITS_SCHEMA_FILE = fileURLToPath(new URL('../../../autotrade/assets/limits.schema.json', import.meta.url));
|
|
30
|
+
export async function limitsCmd(argv, io, deps = {}) {
|
|
31
|
+
const home = dextermHome(io.env);
|
|
32
|
+
const now = deps.now ?? Date.now;
|
|
33
|
+
const sub = argv[0];
|
|
34
|
+
const rest = argv.slice(1);
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case undefined:
|
|
37
|
+
case 'help':
|
|
38
|
+
out(io, LIMITS_USAGE);
|
|
39
|
+
return 0;
|
|
40
|
+
case 'show': return showCmd(rest, io, home, now);
|
|
41
|
+
case 'set': return setCmd(rest, io, home, deps, now);
|
|
42
|
+
case 'diff': return diffCmd(rest, io, home);
|
|
43
|
+
case 'reset': return resetCmd(rest, io, home, deps, now);
|
|
44
|
+
case 'accept': return acceptCmd(io, home, deps, now);
|
|
45
|
+
case 'breaker': {
|
|
46
|
+
if (rest[0] !== 'reset') {
|
|
47
|
+
err(io, 'usage: dexterm autotrade limits breaker reset');
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
const r = loadRiskState(home, now());
|
|
51
|
+
if (!r.breaker) {
|
|
52
|
+
out(io, 'the drawdown breaker is not tripped');
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
out(io, `the drawdown breaker tripped ${new Date(r.breaker.at).toISOString()} at ${r.breaker.drawdownSol} SOL below the peak; resetting it lets new real buys go out again (it also lifts a loss-streak pause and restarts the streak count)`);
|
|
56
|
+
const code = await confirmTyped(io, deps, 'RESET', 'breaker');
|
|
57
|
+
if (!code)
|
|
58
|
+
return 1;
|
|
59
|
+
const x = resetBreaker(home, { confirmed: true, now: now() });
|
|
60
|
+
if (!x.ok) {
|
|
61
|
+
err(io, x.why);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
out(io, 'drawdown breaker reset (journaled); the peak starts again from here; any loss-streak pause is lifted too');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
case 'explain': return explainCmd(rest, io);
|
|
68
|
+
case 'schema':
|
|
69
|
+
out(io, readFileSync(LIMITS_SCHEMA_FILE, 'utf8').trimEnd());
|
|
70
|
+
return 0;
|
|
71
|
+
default:
|
|
72
|
+
err(io, `unknown subcommand ${JSON.stringify(sub)}\n${LIMITS_USAGE}`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function current(home) {
|
|
77
|
+
const l = loadLimits(home);
|
|
78
|
+
if (l.ok)
|
|
79
|
+
return { doc: l.doc, ok: true, note: l.source === 'migrated' ? 'read from card A\'s caps.json (saved as limits.json on your next change)' : l.unjournaled ? `limits.json differs from dexterm's last save (${l.unjournaled.length} change(s)): dexterm autotrade limits diff` : null };
|
|
80
|
+
if (l.missing)
|
|
81
|
+
return { doc: emptyLimits(), ok: true, note: 'no limits saved yet: these are the placeholder defaults, NOT your numbers (real mode will not arm on them silently)' };
|
|
82
|
+
return { doc: emptyLimits(), ok: false, note: l.why };
|
|
83
|
+
}
|
|
84
|
+
function showCmd(argv, io, home, now) {
|
|
85
|
+
const { opts } = parseArgs(argv, ['json', 'all']);
|
|
86
|
+
const mode = opts.mode === 'paper' ? 'paper' : 'real';
|
|
87
|
+
const strategyId = typeof opts.strategy === 'string' ? opts.strategy : '-';
|
|
88
|
+
const accountId = typeof opts.account === 'string' ? opts.account : '-';
|
|
89
|
+
const c = current(home);
|
|
90
|
+
if (!c.ok) {
|
|
91
|
+
err(io, c.note ?? 'limits unreadable');
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const e = effectiveLimits(c.doc, { strategyId, accountId, mode });
|
|
95
|
+
const armed = Object.values(loadArmings(home)).map((a) => ({ id: a.strategyId, accountId: a.accountId, limits: effectiveLimits(c.doc, { strategyId: a.strategyId, accountId: a.accountId, mode: 'real' }).values }));
|
|
96
|
+
const risk = loadRiskState(home, now());
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
out(io, JSON.stringify({ file: limitsPath(home), note: c.note, context: { mode, strategyId, accountId }, applies: e.applies, effective: e.values, from: e.from, scopes: c.doc, armed, risk: { day: risk.day, buys: risk.buys, buySol: risk.buySol, dayPnlSol: risk.dayPnlSol, cumPnlSol: risk.cumPnlSol, lossStreak: risk.lossStreak, pausedUntil: risk.pausedUntil, breaker: risk.breaker } }, null, 2));
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
out(io, `real-mode limits (${limitsPath(home)}) · outside the model: only you change them, here · the defaults are placeholders, your numbers win`);
|
|
102
|
+
if (c.note)
|
|
103
|
+
out(io, ` note: ${c.note}`);
|
|
104
|
+
out(io, `effective for ${mode}${strategyId !== '-' ? ` · strategy ${strategyId}` : ''}${accountId !== '-' ? ` · account ${accountId}` : ''}${e.applies ? '' : ' · paper ignores the limits (add a "paper" scope to make forward tests obey them)'}:`);
|
|
105
|
+
for (const l of limitsLines(e, { all: opts.all === true }))
|
|
106
|
+
out(io, ` ${l}`);
|
|
107
|
+
const scoped = [...(c.doc.modes.real ? ['real'] : []), ...(c.doc.modes.paper ? ['paper'] : []), ...Object.keys(c.doc.strategies).map((id) => `strategy:${id}`), ...Object.keys(c.doc.accounts).map((id) => `account:${id}`)];
|
|
108
|
+
if (scoped.length)
|
|
109
|
+
out(io, ` tighter scopes set: ${scoped.join(', ')} (the stricter number always wins)`);
|
|
110
|
+
for (const a of armed)
|
|
111
|
+
out(io, ` armed ${a.id} (account ${a.accountId}): ${limitsSummary(a.limits)}`);
|
|
112
|
+
out(io, ` today: ${risk.buys} buys · ${risk.buySol} SOL · realized ${risk.dayPnlSol} SOL${risk.breaker ? ' · DRAWDOWN BREAKER TRIPPED (dexterm autotrade limits breaker reset)' : ''}${risk.pausedUntil && risk.pausedUntil > now() ? ` · loss-streak pause until ${new Date(risk.pausedUntil).toISOString().slice(11, 16)} UTC` : ''}`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Print the planned diff (planLimitsSave: measured from dexterm's last journaled save, so a hand edit's raises are
|
|
117
|
+
* shown too) and, when anything is raised, take the typed phrase. Returns the raises confirmed (the exact list
|
|
118
|
+
* saveLimits checks; empty when nothing raises), or null when not confirmed.
|
|
119
|
+
*/
|
|
120
|
+
export async function confirmPlan(io, deps, plan, unjournaled = null, what = 'limits') {
|
|
121
|
+
for (const c of plan.changes)
|
|
122
|
+
out(io, ` ${changeLine(c)}`);
|
|
123
|
+
if (!plan.raised.length)
|
|
124
|
+
return [];
|
|
125
|
+
if (unjournaled && raises(unjournaled).length)
|
|
126
|
+
out(io, `limits.json was changed outside dexterm and that edit RAISES ${raises(unjournaled).map((c) => `${c.scope}.${c.key}`).join(', ')}: it is part of this save (dexterm autotrade limits diff shows it alone)`);
|
|
127
|
+
out(io, `${plan.raised.length} limit(s) RAISED (laxer). Raising applies from the next real run; a running one keeps the stricter number.`);
|
|
128
|
+
return (await confirmTyped(io, deps, 'RAISE', what)) ? plan.raised : null;
|
|
129
|
+
}
|
|
130
|
+
async function setCmd(argv, io, home, deps, now) {
|
|
131
|
+
const { pos, opts } = parseArgs(argv, ['json']);
|
|
132
|
+
if (!pos.length) {
|
|
133
|
+
err(io, 'set name=value … (dexterm autotrade limits explain lists the names)');
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
const c = current(home);
|
|
137
|
+
if (!c.ok) {
|
|
138
|
+
err(io, `${c.note}; fix or remove the file first`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
const next = structuredClone(c.doc);
|
|
142
|
+
delete next.migratedFrom;
|
|
143
|
+
const scope = typeof opts.scope === 'string' ? opts.scope : 'global';
|
|
144
|
+
const ref = scopeRef(next, scope, true);
|
|
145
|
+
if ('error' in ref) {
|
|
146
|
+
err(io, ref.error);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
for (const a of pos) {
|
|
150
|
+
const p = parseLimitAssignment(a);
|
|
151
|
+
if ('error' in p) {
|
|
152
|
+
err(io, p.error);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
if (p.value === null && !ref.isGlobal) {
|
|
156
|
+
delete ref.set[p.key];
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
ref.set[p.key] = p.value;
|
|
160
|
+
}
|
|
161
|
+
const issues = validateLimits(next);
|
|
162
|
+
if (issues.length) {
|
|
163
|
+
for (const i of issues)
|
|
164
|
+
err(io, `${i.path}: ${i.msg}`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
const l = loadLimits(home);
|
|
168
|
+
const plan = planLimitsSave(home, next);
|
|
169
|
+
if (!plan.changes.length && l.ok) {
|
|
170
|
+
out(io, 'unchanged');
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
out(io, `limits ${scope}:`);
|
|
174
|
+
const confirmed = await confirmPlan(io, deps, plan, l.ok ? l.unjournaled : null);
|
|
175
|
+
if (!confirmed)
|
|
176
|
+
return 1;
|
|
177
|
+
const r = saveLimits(home, next, { confirmed, baseline: plan.baseline, now: now() });
|
|
178
|
+
if (!r.ok) {
|
|
179
|
+
err(io, r.why);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
if (opts.json) {
|
|
183
|
+
out(io, JSON.stringify({ saved: true, hash: r.hash, changes: r.changes }, null, 2));
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
out(io, `saved (journaled, ${r.hash}) · lowered limits apply at once, even to a running real run`);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
function diffCmd(argv, io, home) {
|
|
190
|
+
const { opts } = parseArgs(argv, ['json']);
|
|
191
|
+
const l = loadLimits(home);
|
|
192
|
+
let before;
|
|
193
|
+
let after;
|
|
194
|
+
if (typeof opts.file === 'string') {
|
|
195
|
+
let raw;
|
|
196
|
+
try {
|
|
197
|
+
raw = JSON.parse(readFileSync(opts.file, 'utf8'));
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
err(io, `cannot read ${opts.file}: ${e.message.slice(0, 80)}`);
|
|
201
|
+
return 2;
|
|
202
|
+
}
|
|
203
|
+
const issues = validateLimits(raw);
|
|
204
|
+
if (issues.length) {
|
|
205
|
+
for (const i of issues)
|
|
206
|
+
err(io, `${i.path}: ${i.msg}`);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
before = l.ok ? l.doc : null;
|
|
210
|
+
after = raw;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
if (!l.ok) {
|
|
214
|
+
err(io, l.why);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
if (!l.unjournaled) {
|
|
218
|
+
out(io, 'limits.json matches dexterm\'s last save');
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
if (opts.json) {
|
|
222
|
+
out(io, JSON.stringify({ unjournaled: l.unjournaled }, null, 2));
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
out(io, 'limits.json vs dexterm\'s last journaled save:');
|
|
226
|
+
for (const c of l.unjournaled)
|
|
227
|
+
out(io, ` ${changeLine(c)}`);
|
|
228
|
+
if (raises(l.unjournaled).length)
|
|
229
|
+
out(io, 'it RAISES limits: a real run refuses it until you accept it (dexterm autotrade limits accept)');
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
const changes = diffLimits(before, after);
|
|
233
|
+
if (opts.json) {
|
|
234
|
+
out(io, JSON.stringify({ changes }, null, 2));
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
if (!changes.length) {
|
|
238
|
+
out(io, 'no change');
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
for (const c of changes)
|
|
242
|
+
out(io, ` ${changeLine(c)}`);
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
async function resetCmd(argv, io, home, deps, now) {
|
|
246
|
+
const { opts } = parseArgs(argv);
|
|
247
|
+
const c = current(home);
|
|
248
|
+
if (!c.ok) {
|
|
249
|
+
err(io, c.note ?? 'limits unreadable');
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
const next = structuredClone(c.doc);
|
|
253
|
+
delete next.migratedFrom;
|
|
254
|
+
const scope = typeof opts.scope === 'string' ? opts.scope : 'global';
|
|
255
|
+
const ref = scopeRef(next, scope, false);
|
|
256
|
+
if ('error' in ref) {
|
|
257
|
+
err(io, ref.error);
|
|
258
|
+
return 2;
|
|
259
|
+
}
|
|
260
|
+
const key = typeof opts.key === 'string' ? parseLimitAssignment(`${opts.key}=0`) : null;
|
|
261
|
+
if (key && 'error' in key) {
|
|
262
|
+
err(io, key.error);
|
|
263
|
+
return 2;
|
|
264
|
+
}
|
|
265
|
+
if (!ref.set) {
|
|
266
|
+
out(io, `${scope} has nothing set`);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
if (key)
|
|
270
|
+
delete ref.set[key.key];
|
|
271
|
+
else if (ref.isGlobal)
|
|
272
|
+
next.global = {};
|
|
273
|
+
else if (scope === 'real' || scope === 'paper')
|
|
274
|
+
delete next.modes[scope];
|
|
275
|
+
else {
|
|
276
|
+
const [kind, id] = scope.split(':');
|
|
277
|
+
delete next[kind === 'strategy' ? 'strategies' : 'accounts'][id];
|
|
278
|
+
}
|
|
279
|
+
const l = loadLimits(home);
|
|
280
|
+
const plan = planLimitsSave(home, next);
|
|
281
|
+
if (!plan.changes.length) {
|
|
282
|
+
out(io, 'unchanged');
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
out(io, `reset ${scope}${key ? `.${key.key}` : ''} (global numbers fall back to the placeholder defaults):`);
|
|
286
|
+
const confirmed = await confirmPlan(io, deps, plan, l.ok ? l.unjournaled : null);
|
|
287
|
+
if (!confirmed)
|
|
288
|
+
return 1;
|
|
289
|
+
const r = saveLimits(home, next, { confirmed, baseline: plan.baseline, action: 'limits-reset', now: now() });
|
|
290
|
+
if (!r.ok) {
|
|
291
|
+
err(io, r.why);
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
out(io, `saved (journaled, ${r.hash})`);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
async function acceptCmd(io, home, deps, now) {
|
|
298
|
+
const l = loadLimits(home);
|
|
299
|
+
if (!l.ok) {
|
|
300
|
+
err(io, l.why);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
if (!l.unjournaled) {
|
|
304
|
+
out(io, 'limits.json already matches dexterm\'s last save');
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
out(io, 'accept limits.json as it is now:');
|
|
308
|
+
const plan = planLimitsSave(home, l.doc);
|
|
309
|
+
const confirmed = await confirmPlan(io, deps, plan);
|
|
310
|
+
if (!confirmed)
|
|
311
|
+
return 1;
|
|
312
|
+
// task-1654 r4 (verify F4): belt and braces. The plan and the integrity check share one baseline, so they agree;
|
|
313
|
+
// should they ever not, an outside edit that raises anything still needs the typed phrase here.
|
|
314
|
+
const outside = raises(l.unjournaled).map((c) => `${c.scope}.${c.key}`);
|
|
315
|
+
if (outside.some((k) => !confirmed.includes(k))) {
|
|
316
|
+
err(io, `the edit raises ${outside.join(', ')} but the planned save does not show it: nothing accepted (dexterm autotrade limits diff)`);
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const r = saveLimits(home, l.doc, { confirmed, baseline: plan.baseline, action: 'limits-accept', now: now() });
|
|
320
|
+
if (!r.ok) {
|
|
321
|
+
err(io, r.why);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
out(io, `accepted (journaled, ${r.hash})`);
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
function explainCmd(argv, io) {
|
|
328
|
+
const want = argv[0] ? parseLimitAssignment(`${argv[0]}=0`) : null;
|
|
329
|
+
if (want && 'error' in want) {
|
|
330
|
+
err(io, want.error);
|
|
331
|
+
return 2;
|
|
332
|
+
}
|
|
333
|
+
for (const f of LIMIT_FIELDS) {
|
|
334
|
+
if (want && f.key !== want.key)
|
|
335
|
+
continue;
|
|
336
|
+
const d = DEFAULT_LIMITS[f.key];
|
|
337
|
+
out(io, `${f.key} (${f.group}, ${f.unit}, ${f.dir === 'max' ? 'a cap: smaller is stricter' : 'a floor: larger is stricter'}; placeholder ${d === undefined ? 'off' : d})`);
|
|
338
|
+
out(io, ` ${f.help}`);
|
|
339
|
+
}
|
|
340
|
+
if (!want)
|
|
341
|
+
out(io, 'scopes: global (replaces the placeholder) · real / paper (a mode) · strategy:<id> · account:<id>; every scope but global can only tighten');
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
// ───────────────────────────── the first-run wizard (arming) ─────────────────────────────
|
|
345
|
+
/** The numbers the wizard asks for, in order. Everything else keeps its placeholder (shown) or stays off. */
|
|
346
|
+
export const WIZARD_KEYS = ['perTradeSol', 'dailySol', 'buysPerDay', 'maxOpenPositions', 'dailyLossSol', 'executorSilenceMin'];
|
|
347
|
+
/**
|
|
348
|
+
* No limits saved yet: ask for YOUR numbers on the real terminal (Enter keeps the placeholder shown in brackets).
|
|
349
|
+
* Returns the `global` scope to save once the arming phrase is typed, or null (cancelled / not a terminal).
|
|
350
|
+
*/
|
|
351
|
+
export async function limitsWizard(io, deps) {
|
|
352
|
+
if (!isTty(io, deps))
|
|
353
|
+
return null;
|
|
354
|
+
out(io, 'FIRST REAL RUN: set your own limits (they live outside the model; only you change them, on this terminal).');
|
|
355
|
+
out(io, 'The numbers in brackets are placeholders, not advice. Enter keeps one; "off" turns a limit off.');
|
|
356
|
+
const g = {};
|
|
357
|
+
for (const k of WIZARD_KEYS) {
|
|
358
|
+
const f = limitField(k);
|
|
359
|
+
let done = false;
|
|
360
|
+
for (let tries = 0; tries < 3 && !done; tries++) {
|
|
361
|
+
const ans = await typedLine(io, deps, ` ${k}: ${f.help}\n ${f.unit} [${DEFAULT_LIMITS[k] ?? 'off'}]: `);
|
|
362
|
+
if (ans === null)
|
|
363
|
+
return null;
|
|
364
|
+
const t = ans.trim();
|
|
365
|
+
if (!t) {
|
|
366
|
+
g[k] = DEFAULT_LIMITS[k] ?? null;
|
|
367
|
+
done = true;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const p = parseLimitAssignment(`${k}=${t}`);
|
|
371
|
+
if ('error' in p) {
|
|
372
|
+
err(io, p.error);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const issues = validateLimits({ ...emptyLimits(), global: { ...g, [k]: p.value } });
|
|
376
|
+
const mine = issues.filter((i) => i.path === `/global/${k}`);
|
|
377
|
+
if (mine.length) {
|
|
378
|
+
err(io, mine.map((i) => i.msg).join('; '));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
g[k] = p.value;
|
|
382
|
+
done = true;
|
|
383
|
+
}
|
|
384
|
+
if (!done) {
|
|
385
|
+
err(io, `no valid ${k}; nothing saved`);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const k of LIMIT_KEYS)
|
|
390
|
+
if (!(k in g) && DEFAULT_LIMITS[k] !== undefined)
|
|
391
|
+
g[k] = DEFAULT_LIMITS[k];
|
|
392
|
+
return g;
|
|
393
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// `dexterm autotrade receipt …` (task-1645, 0.2.0 card A): what your executor did with a LIVE TRADE FIRE signal, as
|
|
2
|
+
// DATA only. A receipt never makes dexterm do anything: it moves a real-mode position from `unconfirmed` to
|
|
3
|
+
// executed / rejected / failed and measures the drift between the signal and the fill (ledger, Q screen, audit).
|
|
4
|
+
// Your executor can also append `dexterm-trade-receipt/1` lines to autotrade/real/receipts.jsonl itself.
|
|
5
|
+
import { dextermHome } from '../../../custom/src/index.js';
|
|
6
|
+
import { Outbox, RECEIPT_SCHEMA, RECEIPT_STATUSES, appendReceipt, driftReport, readReceipts, receiptMismatch } from '../../../autotrade/src/index.js';
|
|
7
|
+
import { parseArgs } from "./strategies.js";
|
|
8
|
+
export const RECEIPT_USAGE = 'dexterm autotrade receipt <nonce|signal-id> executed|rejected|failed [--price USD] [--size-sol N] [--tokens N] [--tx SIGNATURE] [--reason TEXT] [--executor NAME] | receipt list [--json]';
|
|
9
|
+
export function receiptCmd(argv, io, o = {}) {
|
|
10
|
+
const { pos, opts } = parseArgs(argv, ['json']);
|
|
11
|
+
const home = dextermHome(io.env);
|
|
12
|
+
const now = o.now ?? Date.now;
|
|
13
|
+
const ob = Outbox.open(home, { channels: [], noLock: true });
|
|
14
|
+
if (pos[0] === 'list' || !pos.length) {
|
|
15
|
+
const d = driftReport(ob.signals(), readReceipts(home).receipts, now());
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
io.stdout.write(`${JSON.stringify(d, null, 2)}\n`);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
if (!d.rows.length) {
|
|
21
|
+
io.stdout.write('no trade-fire signals yet\n');
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
io.stdout.write(`signals vs receipts · executed ${d.counts.executed} · rejected ${d.counts.rejected} · failed ${d.counts.failed} · unconfirmed ${d.unconfirmed}${d.orphanReceipts ? ` · ${d.orphanReceipts} orphan receipt(s) ignored` : ''}\n`);
|
|
25
|
+
for (const r of d.rows.slice(-30))
|
|
26
|
+
io.stdout.write(` #${r.nonce} ${r.side.toUpperCase()} ${r.symbol} ${r.signalSol} SOL · ${r.state}${r.filledSol !== null ? ` · filled ${r.filledSol} SOL (${r.sizeDriftPct}%)` : ''}${r.priceDriftPct !== null ? ` · price ${r.priceDriftPct}%` : ''}${r.conflict ? ' · CONFLICTING receipts' : ''}\n`);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
const [ref, status] = pos;
|
|
30
|
+
if (!RECEIPT_STATUSES.includes(status)) {
|
|
31
|
+
io.stderr.write(`usage: ${RECEIPT_USAGE}\n`);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
const sig = /^\d+$/.test(ref) ? ob.get(Number(ref)) : ob.findById(ref);
|
|
35
|
+
const num = (k) => { if (opts[k] === undefined)
|
|
36
|
+
return null; const v = Number(opts[k]); return Number.isFinite(v) && v > 0 ? v : 'bad'; };
|
|
37
|
+
const price = num('price'), sizeSol = num('size-sol'), tokens = num('tokens');
|
|
38
|
+
if (price === 'bad' || sizeSol === 'bad' || tokens === 'bad') {
|
|
39
|
+
io.stderr.write('dexterm autotrade receipt: --price, --size-sol and --tokens are positive numbers\n');
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
if (!sig) {
|
|
43
|
+
io.stderr.write(`dexterm autotrade receipt: no signal ${JSON.stringify(ref)} in the outbox\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const tx = typeof opts.tx === 'string' ? opts.tx : null;
|
|
47
|
+
const fill = price !== null || sizeSol !== null || tokens !== null || tx ? { priceUsd: price, sizeSol, tokens, txSig: tx } : null;
|
|
48
|
+
const r = { schema: RECEIPT_SCHEMA, signalId: sig.id, nonce: sig.nonce, accountId: sig.accountId, status: status, at: now(), fill, reason: typeof opts.reason === 'string' ? opts.reason : null, executor: typeof opts.executor === 'string' ? opts.executor : null };
|
|
49
|
+
const why = receiptMismatch(r, sig);
|
|
50
|
+
if (why) {
|
|
51
|
+
io.stderr.write(`dexterm autotrade receipt: ${why}\n`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
appendReceipt(home, r);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
io.stderr.write(`dexterm autotrade receipt: ${e.message}\n`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
io.stdout.write(`receipt recorded · #${sig.nonce} ${status} (data only: dexterm places nothing)\n`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// `dexterm autotrade report …` (task-1654, 0.2.0 card E): the daily / weekly digest per strategy and mode.
|
|
2
|
+
//
|
|
3
|
+
// report [--period day|week] [--strategy ID] [--json] print it
|
|
4
|
+
// report --telegram send it to your paired chat through your own bot
|
|
5
|
+
// report --schedule daily|weekly|off a run with the bot on sends it by itself (default off)
|
|
6
|
+
//
|
|
7
|
+
// Read-only over the run ledgers, the outbox and the receipts. The numbers are what they are: the digest always
|
|
8
|
+
// carries the win rate's interval and a sample-size note, never a gain claim.
|
|
9
|
+
import { dextermHome } from '../../../custom/src/index.js';
|
|
10
|
+
import { buildDigest, digestLines, effectiveLimits, executorHealth, loadDigestState, loadLimits, readReceipts, saveDigestState, simExecutor, loadPreset } from '../../../autotrade/src/index.js';
|
|
11
|
+
import { parseArgs } from "./strategies.js";
|
|
12
|
+
import { startTelegramHost } from "./telegram-host.js";
|
|
13
|
+
const out = (io, s) => { io.stdout.write(`${s}\n`); };
|
|
14
|
+
const err = (io, s) => { io.stderr.write(`dexterm autotrade report: ${s}\n`); };
|
|
15
|
+
export function digestFor(home, o) {
|
|
16
|
+
const l = loadLimits(home);
|
|
17
|
+
const e = effectiveLimits(l.ok ? l.doc : null, { strategyId: '-', accountId: '-', mode: 'real' });
|
|
18
|
+
const ex = l.ok ? executorHealth(home, { now: o.now, windowMin: e.values.executorSilenceMin ?? null, receipts: readReceipts(home).receipts }) : null;
|
|
19
|
+
return buildDigest(home, { ...o, executor: ex && (ex.lastSeenAt || l.ok) ? ex : null });
|
|
20
|
+
}
|
|
21
|
+
/** Send lines through the user's own bot (one-shot: start, broadcast, stop). */
|
|
22
|
+
export async function sendDigest(home, lines, o) {
|
|
23
|
+
const t = await startTelegramHost({ home, exec: simExecutor(loadPreset('scalp').execution), say: o.say, ...o.telegram });
|
|
24
|
+
if (!t.ok)
|
|
25
|
+
return { ok: false, why: t.why };
|
|
26
|
+
try {
|
|
27
|
+
const before = t.host.service.audit.events.length;
|
|
28
|
+
await t.host.service.hub.broadcast({ kind: 'text', badge: o.real ? 'LIVE' : 'PAPER', lines }, 'report');
|
|
29
|
+
if (t.host.service.audit.events.slice(before).some((e) => e.kind === 'out_failed'))
|
|
30
|
+
return { ok: false, why: 'telegram send failed' };
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await t.host.stop();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function reportCmd(argv, io, deps = {}) {
|
|
38
|
+
const home = dextermHome(io.env);
|
|
39
|
+
const now = deps.now ?? Date.now;
|
|
40
|
+
const { pos, opts } = parseArgs(argv, ['json', 'telegram']);
|
|
41
|
+
if (pos.length) {
|
|
42
|
+
err(io, `unexpected ${pos.join(' ')}`);
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
if (opts.schedule !== undefined) {
|
|
46
|
+
const v = String(opts.schedule);
|
|
47
|
+
if (!['daily', 'weekly', 'off'].includes(v)) {
|
|
48
|
+
err(io, '--schedule daily|weekly|off');
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const s = loadDigestState(home);
|
|
52
|
+
saveDigestState(home, { ...s, schedule: v });
|
|
53
|
+
out(io, v === 'off' ? 'scheduled digest off' : `scheduled digest: ${v} (sent by a run with your Telegram bot on${v === 'weekly' ? ', on Mondays' : ', after local midnight'})`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const period = opts.period === 'week' ? 'week' : opts.period === undefined || opts.period === 'day' ? 'day' : null;
|
|
57
|
+
if (!period) {
|
|
58
|
+
err(io, '--period day|week');
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
const strategyId = typeof opts.strategy === 'string' ? opts.strategy : undefined;
|
|
62
|
+
const d = digestFor(home, { period, now: now(), strategyId });
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
out(io, JSON.stringify(d, null, 2));
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const lines = digestLines(d);
|
|
68
|
+
for (const l of lines)
|
|
69
|
+
out(io, l);
|
|
70
|
+
if (opts.telegram) {
|
|
71
|
+
const r = await sendDigest(home, lines, { say: (s) => io.stderr.write(`${s}\n`), real: d.strategies.some((x) => x.mode === 'real'), telegram: deps.telegram });
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
err(io, `not sent: ${r.why}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
out(io, 'sent to your paired Telegram chat');
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|