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,682 @@
|
|
|
1
|
+
// dexterm autotrade — real-mode limits, every number user-set (task-1654, 0.2.0 card E). Builds on card A's caps.
|
|
2
|
+
//
|
|
3
|
+
// One file, $DEXTERM_HOME/autotrade/real/limits.json (`dexterm-limits/1`, JSON Schema in assets/limits.schema.json):
|
|
4
|
+
//
|
|
5
|
+
// global the home: a number here REPLACES the placeholder default (null = that limit is off)
|
|
6
|
+
// modes.real real runs modes.paper paper runs (opt-in: paper ignores limits unless this scope exists)
|
|
7
|
+
// strategies.<id> one strategy accounts.<id> one account
|
|
8
|
+
//
|
|
9
|
+
// The effective value of a limit is the placeholder default, replaced by `global`, then the STRICTER of that and
|
|
10
|
+
// every other scope that sets it (mode, strategy, account): a strategy or an account can only tighten. Stricter
|
|
11
|
+
// means smaller for a cap (per-trade SOL, buys per day …) and larger for a floor (min liquidity, min position, the
|
|
12
|
+
// loss-streak pause).
|
|
13
|
+
//
|
|
14
|
+
// The safety model (unchanged from card A): limits live OUTSIDE the model. No MCP tool, decision verb or chat
|
|
15
|
+
// command writes this file; the analyst can read, explain and PROPOSE a diff (limits_propose), never apply it.
|
|
16
|
+
// Lowering is free and takes effect at once (a running real run re-reads the file and takes the stricter of what
|
|
17
|
+
// it started with and what the file says now). Raising anything - a bigger number on a cap, a smaller floor, a
|
|
18
|
+
// limit removed or turned off - needs the typed confirmation on a real terminal and applies from the next run.
|
|
19
|
+
// A file that does not match the last save dexterm journaled (a hand edit, or anything else that wrote it) is
|
|
20
|
+
// accepted only if it lowers; one that raises is refused until `dexterm autotrade limits accept` (typed).
|
|
21
|
+
//
|
|
22
|
+
// The defaults are placeholders, not advice: arming real mode asks for your own numbers first and shows the
|
|
23
|
+
// effective limits, which the typed phrase confirms. Your numbers win.
|
|
24
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { writeJsonAtomic } from "./ledger.js";
|
|
28
|
+
import { localDay } from "./chat/audit.js";
|
|
29
|
+
import { realDir } from "./outbox.js";
|
|
30
|
+
export const LIMITS_SCHEMA = 'dexterm-limits/1';
|
|
31
|
+
export const RISK_STATE_SCHEMA = 'dexterm-real-risk/1';
|
|
32
|
+
export const limitsPath = (home) => join(realDir(home), 'limits.json');
|
|
33
|
+
export const riskStatePath = (home) => join(realDir(home), 'risk.json');
|
|
34
|
+
const legacyCapsPath = (home) => join(realDir(home), 'caps.json');
|
|
35
|
+
const legacyDayPath = (home) => join(realDir(home), 'day.json');
|
|
36
|
+
const journalPath = (home) => join(realDir(home), 'journal.jsonl');
|
|
37
|
+
const BIG = 1e12;
|
|
38
|
+
export const LIMIT_FIELDS = [
|
|
39
|
+
{ key: 'perTradeSol', group: 'sizing', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL', help: 'the most SOL one buy signal may spend (a bigger buy is clamped down to it)' },
|
|
40
|
+
{ key: 'perTradePct', group: 'sizing', dir: 'max', lo: 0.01, hi: 100, unit: '% of book', help: 'the most one buy may spend as a percent of the book (bookSol, else the strategy book); with perTradeSol set too, the smaller wins' },
|
|
41
|
+
{ key: 'dailySol', group: 'sizing', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL/day', help: 'SOL all buy signals may spend per local day (a buy over it is refused)' },
|
|
42
|
+
{ key: 'buysPerDay', group: 'sizing', dir: 'max', lo: 0, hi: 1e6, int: true, unit: 'buys/day', help: 'buy signals per local day (0 = no buys)' },
|
|
43
|
+
{ key: 'maxOpenPositions', group: 'sizing', dir: 'max', lo: 0, hi: 1e5, int: true, unit: 'open', help: 'real positions open at once, across every strategy and account on this home' },
|
|
44
|
+
{ key: 'maxExposurePct', group: 'sizing', dir: 'max', lo: 0.01, hi: 100, unit: '% of book', help: 'the cost of every open real position together, as a percent of the book (a buy is clamped to what is left)' },
|
|
45
|
+
{ key: 'minPositionSol', group: 'sizing', dir: 'min', lo: 0, hi: BIG, unit: 'SOL', help: 'a buy smaller than this after every clamp is refused (no dust orders)' },
|
|
46
|
+
{ key: 'maxPositionSol', group: 'sizing', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL', help: 'the most SOL one position may cost, adds included (a buy is clamped to what is left)' },
|
|
47
|
+
{ key: 'perCoinSolPerDay', group: 'sizing', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL/day', help: 'SOL spent on one coin per local day, re-entries included (a buy over it is refused)' },
|
|
48
|
+
{ key: 'perCoinBuysPerDay', group: 'sizing', dir: 'max', lo: 0, hi: 1e5, int: true, unit: 'buys/day', help: 'buy signals on one coin per local day' },
|
|
49
|
+
{ key: 'bookSol', group: 'sizing', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL', help: 'what the percent limits are a percent of: the size of YOUR account (unset = the strategy\'s own book)' },
|
|
50
|
+
{ key: 'dailyLossSol', group: 'risk', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL/day', help: 'when the day\'s realized loss reaches this, no new buy goes out until the next local day (exits still do)' },
|
|
51
|
+
{ key: 'maxDrawdownSol', group: 'risk', dir: 'max', lo: 0.000001, hi: BIG, unit: 'SOL', help: 'the drawdown breaker: realized PnL this far below its peak trips it; no new buy until you reset it (typed)' },
|
|
52
|
+
{ key: 'lossStreak', group: 'risk', dir: 'max', lo: 1, hi: 1e4, int: true, unit: 'losses', help: 'this many losing closes in a row pause new buys for lossStreakPauseMin' },
|
|
53
|
+
{ key: 'lossStreakPauseMin', group: 'risk', dir: 'min', lo: 1, hi: 525_600, unit: 'min', help: 'how long the loss-streak pause lasts' },
|
|
54
|
+
{ key: 'maxHoldMinutes', group: 'risk', dir: 'max', lo: 1, hi: 525_600, unit: 'min', help: 'a real position held this long gets its exit signal (the stricter of this and the exit profile\'s own cap)' },
|
|
55
|
+
{ key: 'minLiquidityUsd', group: 'risk', dir: 'min', lo: 0, hi: BIG, unit: 'USD', help: 'no buy on a coin with less pool liquidity (unknown liquidity is refused while this is set)' },
|
|
56
|
+
{ key: 'minMcapUsd', group: 'risk', dir: 'min', lo: 0, hi: BIG, unit: 'USD', help: 'no buy on a coin below this market cap (unknown is refused while this is set)' },
|
|
57
|
+
{ key: 'maxSlippageBps', group: 'risk', dir: 'max', lo: 1, hi: 10_000, int: true, unit: 'bps', help: 'the slippage tolerance a signal states (the stricter of this and the strategy\'s)' },
|
|
58
|
+
{ key: 'maxPriceImpactPct', group: 'risk', dir: 'max', lo: 0.01, hi: 100, unit: '%', help: 'the price impact a buy may have on the pool (the engine\'s constant-product model: size / half the liquidity): a buy is clamped to it, and the signal states the stricter tolerance' },
|
|
59
|
+
{ key: 'signalTtlSec', group: 'risk', dir: 'max', lo: 10, hi: 600, int: true, unit: 's', help: 'how long an executor may act on a signal (the stricter of this and the channel setting)' },
|
|
60
|
+
{ key: 'maxBuysPerMinute', group: 'risk', dir: 'max', lo: 0, hi: 1e4, int: true, unit: 'buys/min', help: 'the signal rate limit: buy signals in any 60 s' },
|
|
61
|
+
{ key: 'maxSellsPerMinute', group: 'sells', dir: 'max', lo: 1, hi: 1e4, int: true, unit: 'sells/min', help: 'OPTIONAL: sells are never capped unless you set this; a sell over it waits for the next tick' },
|
|
62
|
+
{ key: 'minForwardTrades', group: 'risk', dir: 'min', lo: 0, hi: 1e5, int: true, unit: 'trades', help: 'the evidence floor for real: arming needs a COMPLETED forward test of these exact rules with at least this many closed paper trades, and a backtest on record (below it only a typed override on your own terminal arms, journaled)' },
|
|
63
|
+
{ key: 'executorSilenceMin', group: 'executor', dir: 'max', lo: 1, hi: 525_600, unit: 'min', help: 'no heartbeat, receipt or ack from your executor for this long pauses new buy signals (exits still go out)' },
|
|
64
|
+
];
|
|
65
|
+
export const LIMIT_KEYS = LIMIT_FIELDS.map((f) => f.key);
|
|
66
|
+
const FIELD = new Map(LIMIT_FIELDS.map((f) => [f.key, f]));
|
|
67
|
+
export const limitField = (k) => FIELD.get(k);
|
|
68
|
+
/**
|
|
69
|
+
* The placeholder defaults for real mode. Conservative for a first real run, not advice: arming asks for your own
|
|
70
|
+
* numbers and shows these only as the starting point. Every other limit is unset (off) until you set it.
|
|
71
|
+
*/
|
|
72
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
73
|
+
perTradeSol: 0.25, dailySol: 1, buysPerDay: 20, maxOpenPositions: 3,
|
|
74
|
+
dailyLossSol: 0.5, lossStreak: 5, lossStreakPauseMin: 60, maxBuysPerMinute: 3, executorSilenceMin: 15,
|
|
75
|
+
// task-1679 (the 0.2.0 product decision, 2026-10-25): the evidence floor for promotion to real.
|
|
76
|
+
minForwardTrades: 10,
|
|
77
|
+
});
|
|
78
|
+
export const emptyLimits = () => ({ schema: LIMITS_SCHEMA, global: {}, modes: {}, strategies: {}, accounts: {} });
|
|
79
|
+
const SCOPE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
80
|
+
const DOC_KEYS = new Set(['schema', 'global', 'modes', 'strategies', 'accounts', 'updatedAt', 'migratedFrom']);
|
|
81
|
+
function validateSet(x, path, allowNull, out) {
|
|
82
|
+
if (!x || typeof x !== 'object' || Array.isArray(x)) {
|
|
83
|
+
out.push({ path, msg: 'must be an object of limits' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const [k, v] of Object.entries(x)) {
|
|
87
|
+
const f = FIELD.get(k);
|
|
88
|
+
const p = `${path}/${k}`;
|
|
89
|
+
if (!f) {
|
|
90
|
+
out.push({ path: p, msg: `unknown limit "${k}" (known: ${LIMIT_KEYS.join(', ')})` });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (v === null) {
|
|
94
|
+
if (!allowNull)
|
|
95
|
+
out.push({ path: p, msg: `${k}: null (off) is only allowed in global; remove the key to leave it unset here` });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
99
|
+
out.push({ path: p, msg: `${k} must be a number (${f.unit})` });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (v < f.lo || v > f.hi) {
|
|
103
|
+
out.push({ path: p, msg: `${k} must be from ${f.lo} to ${f.hi} ${f.unit}` });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (f.int && !Number.isInteger(v))
|
|
107
|
+
out.push({ path: p, msg: `${k} must be a whole number` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Every problem with a limits document, each naming the field (`/strategies/scalp/perTradeSol …`). */
|
|
111
|
+
export function validateLimits(x) {
|
|
112
|
+
const out = [];
|
|
113
|
+
const d = x;
|
|
114
|
+
if (!d || typeof d !== 'object' || Array.isArray(d))
|
|
115
|
+
return [{ path: '', msg: 'the limits file is a JSON object' }];
|
|
116
|
+
if (d.schema !== LIMITS_SCHEMA)
|
|
117
|
+
out.push({ path: '/schema', msg: `schema must be "${LIMITS_SCHEMA}"` });
|
|
118
|
+
for (const k of Object.keys(d))
|
|
119
|
+
if (!DOC_KEYS.has(k))
|
|
120
|
+
out.push({ path: `/${k}`, msg: `unknown key "${k}" (scopes: global, modes, strategies, accounts)` });
|
|
121
|
+
validateSet(d.global ?? {}, '/global', true, out);
|
|
122
|
+
const modes = (d.modes ?? {});
|
|
123
|
+
if (typeof modes !== 'object' || Array.isArray(modes))
|
|
124
|
+
out.push({ path: '/modes', msg: 'modes is an object with real and/or paper' });
|
|
125
|
+
else
|
|
126
|
+
for (const [m, v] of Object.entries(modes)) {
|
|
127
|
+
if (m !== 'real' && m !== 'paper')
|
|
128
|
+
out.push({ path: `/modes/${m}`, msg: `unknown mode "${m}" (real or paper)` });
|
|
129
|
+
else
|
|
130
|
+
validateSet(v, `/modes/${m}`, false, out);
|
|
131
|
+
}
|
|
132
|
+
for (const scope of ['strategies', 'accounts']) {
|
|
133
|
+
const m = (d[scope] ?? {});
|
|
134
|
+
if (typeof m !== 'object' || Array.isArray(m)) {
|
|
135
|
+
out.push({ path: `/${scope}`, msg: `${scope} is an object keyed by id` });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const [id, v] of Object.entries(m)) {
|
|
139
|
+
if (!SCOPE_ID.test(id))
|
|
140
|
+
out.push({ path: `/${scope}/${id}`, msg: `"${id}" is not a valid id` });
|
|
141
|
+
else
|
|
142
|
+
validateSet(v, `/${scope}/${id}`, false, out);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!out.length)
|
|
146
|
+
out.push(...crossCheck(d));
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Rules across fields, on the effective numbers of every scope combination the file names. */
|
|
150
|
+
function crossCheck(d) {
|
|
151
|
+
const out = [];
|
|
152
|
+
const g = { ...DEFAULT_LIMITS, ...d.global };
|
|
153
|
+
if (num(g.perTradeSol) !== null && num(g.dailySol) !== null && g.dailySol < g.perTradeSol)
|
|
154
|
+
out.push({ path: '/global/dailySol', msg: `dailySol (${g.dailySol}) must be at least perTradeSol (${g.perTradeSol})` });
|
|
155
|
+
if (num(g.minPositionSol) !== null && num(g.perTradeSol) !== null && g.minPositionSol > g.perTradeSol)
|
|
156
|
+
out.push({ path: '/global/minPositionSol', msg: `minPositionSol (${g.minPositionSol}) is above perTradeSol (${g.perTradeSol}): every buy would be refused` });
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
160
|
+
/** Normalize a valid document (drop empty scopes, sort keys) so its hash is stable. */
|
|
161
|
+
export function normalizeLimits(d) {
|
|
162
|
+
const set = (s) => Object.fromEntries(LIMIT_KEYS.filter((k) => s && k in s).map((k) => [k, s[k]]));
|
|
163
|
+
const map = (m) => Object.fromEntries(Object.keys(m ?? {}).sort().map((id) => [id, set(m[id])]).filter(([, s]) => Object.keys(s).length));
|
|
164
|
+
const modes = {};
|
|
165
|
+
if (d.modes?.real)
|
|
166
|
+
modes.real = set(d.modes.real);
|
|
167
|
+
if (d.modes?.paper)
|
|
168
|
+
modes.paper = set(d.modes.paper); // an empty paper scope still means "apply to paper"
|
|
169
|
+
return { schema: LIMITS_SCHEMA, global: set(d.global), modes, strategies: map(d.strategies), accounts: map(d.accounts), ...(d.migratedFrom ? { migratedFrom: d.migratedFrom } : {}) };
|
|
170
|
+
}
|
|
171
|
+
export const limitsHash = (d) => createHash('sha256').update(JSON.stringify({ ...normalizeLimits(d), migratedFrom: undefined })).digest('hex').slice(0, 16);
|
|
172
|
+
const stricter = (k, a, b) => (FIELD.get(k).dir === 'max' ? Math.min(a, b) : Math.max(a, b));
|
|
173
|
+
export function effectiveLimits(doc, c) {
|
|
174
|
+
const d = doc ?? emptyLimits();
|
|
175
|
+
if (c.mode === 'paper' && !d.modes.paper)
|
|
176
|
+
return { values: {}, from: {}, applies: false };
|
|
177
|
+
const values = {};
|
|
178
|
+
const from = {};
|
|
179
|
+
for (const k of LIMIT_KEYS) {
|
|
180
|
+
if (k in (d.global ?? {})) {
|
|
181
|
+
const v = d.global[k];
|
|
182
|
+
if (v !== null && v !== undefined) {
|
|
183
|
+
values[k] = v;
|
|
184
|
+
from[k] = 'global';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (DEFAULT_LIMITS[k] !== undefined) {
|
|
188
|
+
values[k] = DEFAULT_LIMITS[k];
|
|
189
|
+
from[k] = 'default';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const layers = [[`modes.${c.mode}`, d.modes[c.mode]], [`strategies.${c.strategyId}`, d.strategies[c.strategyId]], [`accounts.${c.accountId}`, d.accounts[c.accountId]]];
|
|
193
|
+
for (const [name, s] of layers) {
|
|
194
|
+
if (!s)
|
|
195
|
+
continue;
|
|
196
|
+
for (const k of LIMIT_KEYS) {
|
|
197
|
+
const v = s[k];
|
|
198
|
+
if (typeof v !== 'number')
|
|
199
|
+
continue;
|
|
200
|
+
const cur = values[k];
|
|
201
|
+
const next = cur === undefined ? v : stricter(k, cur, v);
|
|
202
|
+
if (next !== cur) {
|
|
203
|
+
values[k] = next;
|
|
204
|
+
from[k] = name;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { values, from, applies: true };
|
|
209
|
+
}
|
|
210
|
+
/** The stricter of two effective sets, limit by limit (a run: what it started with vs the file now). */
|
|
211
|
+
export function strictestOf(a, b) {
|
|
212
|
+
const out = {};
|
|
213
|
+
for (const k of LIMIT_KEYS) {
|
|
214
|
+
const x = a[k];
|
|
215
|
+
const y = b[k];
|
|
216
|
+
if (x === undefined && y === undefined)
|
|
217
|
+
continue;
|
|
218
|
+
out[k] = x === undefined ? y : y === undefined ? x : stricter(k, x, y);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
/** A scope's value as a strictness-comparable number: unset = no limit (+inf for a cap, -inf for a floor). */
|
|
223
|
+
function asBound(k, v) {
|
|
224
|
+
if (typeof v === 'number')
|
|
225
|
+
return v;
|
|
226
|
+
return FIELD.get(k).dir === 'max' ? Infinity : -Infinity;
|
|
227
|
+
}
|
|
228
|
+
/** Every changed number, scope by scope; `raise` = laxer (needs the typed confirmation), `lower` = stricter. */
|
|
229
|
+
export function diffLimits(before, afterIn) {
|
|
230
|
+
const b = before ? normalizeLimits(before) : emptyLimits();
|
|
231
|
+
const after = normalizeLimits(afterIn);
|
|
232
|
+
const out = [];
|
|
233
|
+
const cmp = (scope, x, y, isGlobal) => {
|
|
234
|
+
for (const k of LIMIT_KEYS) {
|
|
235
|
+
// In global an absent number means the placeholder default.
|
|
236
|
+
const fx = isGlobal && !(x && k in x) ? (DEFAULT_LIMITS[k] ?? undefined) : x?.[k];
|
|
237
|
+
const fy = isGlobal && !(y && k in y) ? (DEFAULT_LIMITS[k] ?? undefined) : y?.[k];
|
|
238
|
+
const bx = asBound(k, fx);
|
|
239
|
+
const by = asBound(k, fy);
|
|
240
|
+
if (bx === by)
|
|
241
|
+
continue;
|
|
242
|
+
const laxer = FIELD.get(k).dir === 'max' ? by > bx : by < bx;
|
|
243
|
+
out.push({ scope, key: k, from: fx ?? null, to: fy ?? null, kind: laxer ? 'raise' : 'lower' });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
cmp('global', b.global, after.global, true);
|
|
247
|
+
// A paper scope appearing makes paper runs obey limits (stricter); removing it frees them (a raise).
|
|
248
|
+
if (!!b.modes.paper !== !!after.modes.paper)
|
|
249
|
+
out.push({ scope: 'modes.paper', key: 'scope', from: null, to: null, kind: after.modes.paper ? 'lower' : 'raise' });
|
|
250
|
+
for (const m of ['real', 'paper'])
|
|
251
|
+
cmp(`modes.${m}`, b.modes[m], after.modes[m], false);
|
|
252
|
+
for (const scope of ['strategies', 'accounts']) {
|
|
253
|
+
for (const id of new Set([...Object.keys(b[scope]), ...Object.keys(after[scope])]))
|
|
254
|
+
cmp(`${scope}.${id}`, b[scope][id], after[scope][id], false);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
export const raises = (c) => c.filter((x) => x.kind === 'raise');
|
|
259
|
+
export function changeLine(c) {
|
|
260
|
+
if (c.key === 'scope')
|
|
261
|
+
return c.kind === 'raise' ? 'RAISE modes.paper removed: paper runs stop obeying the limits' : 'lower modes.paper added: paper runs obey the limits too';
|
|
262
|
+
const f = FIELD.get(c.key);
|
|
263
|
+
const v = (x) => (x === null ? 'off' : `${x} ${f.unit}`);
|
|
264
|
+
return `${c.kind === 'raise' ? 'RAISE' : 'lower'} ${c.scope}.${c.key}: ${v(c.from)} -> ${v(c.to)}`;
|
|
265
|
+
}
|
|
266
|
+
function journal(home, line) {
|
|
267
|
+
mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
|
|
268
|
+
appendFileSync(journalPath(home), `${JSON.stringify(line)}\n`, { mode: 0o600 });
|
|
269
|
+
}
|
|
270
|
+
/** The last limits document dexterm saved itself (from the journal), with its hash. */
|
|
271
|
+
/**
|
|
272
|
+
* card A's last journaled `signal caps set` (a `caps-set` line with `after` = the four caps and no hash), or null.
|
|
273
|
+
* Card A wrote caps.json only through that command, so a genuine caps.json always matches it.
|
|
274
|
+
*/
|
|
275
|
+
function lastCardACaps(home) {
|
|
276
|
+
if (!existsSync(journalPath(home)))
|
|
277
|
+
return null;
|
|
278
|
+
const lines = readFileSync(journalPath(home), 'utf8').split('\n').filter(Boolean);
|
|
279
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
280
|
+
try {
|
|
281
|
+
const l = JSON.parse(lines[i]);
|
|
282
|
+
if (l.action !== 'caps-set' || typeof l.data.hash === 'string')
|
|
283
|
+
continue;
|
|
284
|
+
const a = l.data.after;
|
|
285
|
+
if (!a)
|
|
286
|
+
return null;
|
|
287
|
+
const s = {};
|
|
288
|
+
for (const k of ['perTradeSol', 'dailySol', 'buysPerDay', 'maxOpenPositions'])
|
|
289
|
+
if (typeof a[k] === 'number')
|
|
290
|
+
s[k] = a[k];
|
|
291
|
+
return Object.keys(s).length === 4 ? s : null;
|
|
292
|
+
}
|
|
293
|
+
catch { /* torn line */ }
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function lastJournaled(home) {
|
|
298
|
+
if (!existsSync(journalPath(home)))
|
|
299
|
+
return null;
|
|
300
|
+
const lines = readFileSync(journalPath(home), 'utf8').split('\n').filter(Boolean);
|
|
301
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
302
|
+
try {
|
|
303
|
+
const l = JSON.parse(lines[i]);
|
|
304
|
+
if (['limits-set', 'limits-accept', 'limits-reset', 'limits-migrated', 'caps-set'].includes(l.action) && typeof l.data.hash === 'string')
|
|
305
|
+
return { hash: l.data.hash, doc: l.data.after ?? null };
|
|
306
|
+
}
|
|
307
|
+
catch { /* torn line */ }
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
/** card A's caps.json, if that is all there is. */
|
|
312
|
+
function legacyCaps(home) {
|
|
313
|
+
try {
|
|
314
|
+
const d = JSON.parse(readFileSync(legacyCapsPath(home), 'utf8'));
|
|
315
|
+
if (d.schema !== 'dexterm-real-caps/1' || !d.caps)
|
|
316
|
+
return null;
|
|
317
|
+
const s = {};
|
|
318
|
+
for (const k of ['perTradeSol', 'dailySol', 'buysPerDay', 'maxOpenPositions'])
|
|
319
|
+
if (typeof d.caps[k] === 'number')
|
|
320
|
+
s[k] = d.caps[k];
|
|
321
|
+
return Object.keys(s).length === 4 ? s : null;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The limits file. Missing: card A's caps.json is migrated (read as `global`; written on the next save), else
|
|
329
|
+
* { missing }. Invalid or unreadable: not ok, and a real run refuses to start (fail closed). `unjournaled` lists how
|
|
330
|
+
* the file differs from dexterm's last journaled save when it does (null when it matches).
|
|
331
|
+
*/
|
|
332
|
+
export function loadLimits(home) {
|
|
333
|
+
const p = limitsPath(home);
|
|
334
|
+
if (!existsSync(p)) {
|
|
335
|
+
const caps = legacyCaps(home);
|
|
336
|
+
if (!caps)
|
|
337
|
+
return { ok: false, missing: true, why: 'no real-mode limits are set yet: arming asks for your numbers (dexterm autotrade signal arm <id>), or dexterm autotrade limits set …' };
|
|
338
|
+
// task-1654 r3 (verify F3): card A's caps.json is a migration only when it equals card A's own journaled caps
|
|
339
|
+
// and no limits save exists; anything else is an outside edit, measured from the integrity baseline.
|
|
340
|
+
return integrityOf(home, { ...emptyLimits(), global: caps, migratedFrom: 'caps.json' }, 'migrated');
|
|
341
|
+
}
|
|
342
|
+
let raw;
|
|
343
|
+
try {
|
|
344
|
+
raw = JSON.parse(readFileSync(p, 'utf8'));
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
return { ok: false, why: `${p} is unreadable (${e.message.slice(0, 60)}): real buys stay refused until it is fixed` };
|
|
348
|
+
}
|
|
349
|
+
const issues = validateLimits(raw);
|
|
350
|
+
if (issues.length)
|
|
351
|
+
return { ok: false, issues, why: `limits: ${issues.slice(0, 3).map((i) => `${i.path} ${i.msg}`).join('; ')}` };
|
|
352
|
+
return integrityOf(home, normalizeLimits(raw), 'file');
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* task-1654 r4 (verify F4): the ONE integrity baseline. What loadLimits measures an outside edit from AND what every
|
|
356
|
+
* save is planned and checked against (planLimitsSave / saveLimits), so the diff a user confirms is always the diff
|
|
357
|
+
* the real run's check measures:
|
|
358
|
+
* dexterm's last hashed limits save
|
|
359
|
+
* else card A's own last journaled caps (`signal caps set`, as `global`)
|
|
360
|
+
* else null: the placeholder defaults.
|
|
361
|
+
* Never the file itself: a file nobody journaled is an outside edit until it is saved or accepted.
|
|
362
|
+
*/
|
|
363
|
+
function integrityBaseline(home) {
|
|
364
|
+
const last = lastJournaled(home);
|
|
365
|
+
if (last)
|
|
366
|
+
return { doc: last.doc, hash: last.hash };
|
|
367
|
+
const a = lastCardACaps(home);
|
|
368
|
+
return { doc: a ? { ...emptyLimits(), global: a } : null, hash: null };
|
|
369
|
+
}
|
|
370
|
+
function integrityOf(home, doc, source) {
|
|
371
|
+
const hash = limitsHash(doc);
|
|
372
|
+
const base = integrityBaseline(home);
|
|
373
|
+
if (base.hash !== null && base.hash === hash)
|
|
374
|
+
return { ok: true, doc, source, hash, journaled: true, unjournaled: null };
|
|
375
|
+
const changes = diffLimits(base.doc, doc);
|
|
376
|
+
// Against a hashed save, any other hash is unjournaled (its changes may be empty); otherwise no change = journaled.
|
|
377
|
+
const journaled = base.hash === null && !changes.length;
|
|
378
|
+
return { ok: true, doc, source, hash, journaled, unjournaled: journaled ? null : changes };
|
|
379
|
+
}
|
|
380
|
+
/** mtime of the file, to re-read it only when it changed. */
|
|
381
|
+
export function limitsStamp(home) { try {
|
|
382
|
+
return statSync(limitsPath(home)).mtimeMs;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return 0;
|
|
386
|
+
} }
|
|
387
|
+
function baselineOf(home) {
|
|
388
|
+
return integrityBaseline(home).doc;
|
|
389
|
+
}
|
|
390
|
+
const raiseKey = (c) => `${c.scope}.${c.key}`;
|
|
391
|
+
/**
|
|
392
|
+
* task-1654 r2 (verify F1): the diff a save would make. Any unjournaled hand edit in the file shows up here too (it
|
|
393
|
+
* is measured from the last journaled save, not from the file), so a lowering `set` on top of a hand-edit raise is a
|
|
394
|
+
* raise, and the typed confirmation covers every line shown, never only the line the user typed.
|
|
395
|
+
*/
|
|
396
|
+
export function planLimitsSave(home, next) {
|
|
397
|
+
const before = baselineOf(home);
|
|
398
|
+
const changes = diffLimits(before, normalizeLimits(next));
|
|
399
|
+
return { before, baseline: before ? limitsHash(before) : 'none', changes, raised: raises(changes).map(raiseKey) };
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Save a new limits document. Every raise needs confirming: `confirmed` is the list of raises (`scope.key`, from
|
|
403
|
+
* planLimitsSave) the user confirmed with the typed phrase on a real terminal; there is no "confirm everything"
|
|
404
|
+
* (task-1654 r3, verify F2: a phrase confirms only the lines shown). Lowering needs nothing. With `baseline`, the save
|
|
405
|
+
* refuses when dexterm's last save changed since the plan. Journaled with the document and its hash.
|
|
406
|
+
*/
|
|
407
|
+
export function saveLimits(home, next, o) {
|
|
408
|
+
const issues = validateLimits(next);
|
|
409
|
+
if (issues.length)
|
|
410
|
+
return { ok: false, issues, why: issues.map((i) => `${i.path}: ${i.msg}`).join('; ') };
|
|
411
|
+
const doc = normalizeLimits(next);
|
|
412
|
+
const before = baselineOf(home);
|
|
413
|
+
if (o.baseline !== undefined && o.baseline !== (before ? limitsHash(before) : 'none'))
|
|
414
|
+
return { ok: false, why: 'the limits changed while you were confirming (another save): nothing saved; run the command again' };
|
|
415
|
+
const changes = diffLimits(before, doc);
|
|
416
|
+
const up = raises(changes);
|
|
417
|
+
const unconfirmed = up.filter((c) => !o.confirmed.includes(raiseKey(c)));
|
|
418
|
+
if (unconfirmed.length)
|
|
419
|
+
return { ok: false, changes, why: `raising ${unconfirmed.map(raiseKey).join(', ')} needs the typed confirmation on a real terminal` };
|
|
420
|
+
const now = o.now ?? Date.now();
|
|
421
|
+
mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
|
|
422
|
+
writeJsonAtomic(limitsPath(home), { ...doc, updatedAt: now });
|
|
423
|
+
const hash = limitsHash(doc);
|
|
424
|
+
journal(home, { t: now, action: o.action ?? 'limits-set', by: o.by ?? 'local-terminal', data: { hash, before, after: doc, raised: up.map((c) => `${c.scope}.${c.key}`), changes: changes.map(changeLine) } });
|
|
425
|
+
return { ok: true, changes, hash };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* A file that differs from the last journaled save: lower-only edits are accepted (journaled `limits-accept`, by the
|
|
429
|
+
* runner); one that raises anything is refused until `dexterm autotrade limits accept` (typed).
|
|
430
|
+
*/
|
|
431
|
+
export function limitsForRealRun(home, now = Date.now()) {
|
|
432
|
+
const l = loadLimits(home);
|
|
433
|
+
if (!l.ok)
|
|
434
|
+
return { ok: false, why: l.why };
|
|
435
|
+
if (l.unjournaled) {
|
|
436
|
+
const up = raises(l.unjournaled);
|
|
437
|
+
if (up.length)
|
|
438
|
+
return { ok: false, why: `${l.source === 'migrated' ? 'limits.json is missing and caps.json' : 'limits.json'} was changed outside dexterm and RAISES ${up.map((c) => `${c.scope}.${c.key}`).join(', ')}: review it (dexterm autotrade limits diff) and accept it on your own terminal (dexterm autotrade limits accept)` };
|
|
439
|
+
journal(home, { t: now, action: 'limits-accept', by: 'runner', data: { hash: l.hash, after: l.doc, changes: l.unjournaled.map(changeLine), why: 'lower-only edit outside dexterm' } });
|
|
440
|
+
}
|
|
441
|
+
return { ok: true, doc: l.doc, hash: l.hash };
|
|
442
|
+
}
|
|
443
|
+
/** Parse `perTradeSol=0.5` / `per-trade-sol=0.5` / `dailyLossSol=off`. */
|
|
444
|
+
export function parseLimitAssignment(s) {
|
|
445
|
+
const m = /^([A-Za-z-]+)=(.+)$/.exec(s.trim());
|
|
446
|
+
if (!m)
|
|
447
|
+
return { error: `"${s}" is not name=value` };
|
|
448
|
+
const camel = m[1].replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
449
|
+
const f = FIELD.get(camel);
|
|
450
|
+
if (!f)
|
|
451
|
+
return { error: `unknown limit "${m[1]}" (dexterm autotrade limits show --all lists them)` };
|
|
452
|
+
if (m[2] === 'off' || m[2] === 'null')
|
|
453
|
+
return { key: f.key, value: null };
|
|
454
|
+
const v = Number(m[2]);
|
|
455
|
+
if (!Number.isFinite(v))
|
|
456
|
+
return { error: `${f.key}: "${m[2]}" is not a number` };
|
|
457
|
+
return { key: f.key, value: v };
|
|
458
|
+
}
|
|
459
|
+
/** Where a scope lives in the document (`global`, `real`/`paper`, `strategy:<id>`, `account:<id>`). */
|
|
460
|
+
export function scopeRef(doc, scope, create) {
|
|
461
|
+
if (scope === 'global')
|
|
462
|
+
return { set: doc.global, name: 'global', isGlobal: true };
|
|
463
|
+
if (scope === 'real' || scope === 'paper') {
|
|
464
|
+
if (create)
|
|
465
|
+
doc.modes[scope] ??= {};
|
|
466
|
+
return { set: doc.modes[scope], name: `modes.${scope}`, isGlobal: false };
|
|
467
|
+
}
|
|
468
|
+
const m = /^(strategy|account):(.+)$/.exec(scope);
|
|
469
|
+
if (!m || !SCOPE_ID.test(m[2]))
|
|
470
|
+
return { error: `scope "${scope}": global, real, paper, strategy:<id> or account:<id>` };
|
|
471
|
+
const key = m[1] === 'strategy' ? 'strategies' : 'accounts';
|
|
472
|
+
if (create)
|
|
473
|
+
doc[key][m[2]] ??= {};
|
|
474
|
+
return { set: doc[key][m[2]], name: `${key}.${m[2]}`, isGlobal: false };
|
|
475
|
+
}
|
|
476
|
+
export function freshRiskState(day) {
|
|
477
|
+
return { schema: RISK_STATE_SCHEMA, day, buys: 0, buySol: 0, perCoin: {}, dayPnlSol: 0, cumPnlSol: 0, peakCumPnlSol: 0, lossStreak: 0, pausedUntil: null, breaker: null, recentBuys: [], recentSells: [] };
|
|
478
|
+
}
|
|
479
|
+
/** The persisted guard state, rolled to today (day counters reset; PnL peak, streak and breaker carry). */
|
|
480
|
+
export function loadRiskState(home, now) {
|
|
481
|
+
const today = localDay(now);
|
|
482
|
+
let s = null;
|
|
483
|
+
try {
|
|
484
|
+
const d = JSON.parse(readFileSync(riskStatePath(home), 'utf8'));
|
|
485
|
+
if (d.schema === RISK_STATE_SCHEMA)
|
|
486
|
+
s = d;
|
|
487
|
+
}
|
|
488
|
+
catch { /* none yet */ }
|
|
489
|
+
if (!s) {
|
|
490
|
+
s = freshRiskState(today);
|
|
491
|
+
// card A kept the day's buys in day.json: carry them over once.
|
|
492
|
+
try {
|
|
493
|
+
const d = JSON.parse(readFileSync(legacyDayPath(home), 'utf8'));
|
|
494
|
+
if (d.day === today) {
|
|
495
|
+
s.buys = Number(d.buys) || 0;
|
|
496
|
+
s.buySol = Number(d.buySol) || 0;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch { /* none */ }
|
|
500
|
+
}
|
|
501
|
+
if (s.day !== today)
|
|
502
|
+
s = { ...s, day: today, buys: 0, buySol: 0, perCoin: {}, dayPnlSol: 0 };
|
|
503
|
+
s.recentBuys = (s.recentBuys ?? []).filter((t) => now - t < 60_000);
|
|
504
|
+
s.recentSells = (s.recentSells ?? []).filter((t) => now - t < 60_000);
|
|
505
|
+
return s;
|
|
506
|
+
}
|
|
507
|
+
export function saveRiskState(home, s) {
|
|
508
|
+
mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
|
|
509
|
+
writeJsonAtomic(riskStatePath(home), s);
|
|
510
|
+
}
|
|
511
|
+
const r6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
512
|
+
/** Why new buys are paused right now (loss limits, the breaker, the streak pause), or null. */
|
|
513
|
+
export function buyPause(L, s, now) {
|
|
514
|
+
if (s.breaker)
|
|
515
|
+
return { refused: 'real-drawdown-breaker', detail: `the drawdown breaker tripped (${s.breaker.drawdownSol} SOL below the peak): dexterm autotrade limits breaker reset, on your own terminal` };
|
|
516
|
+
if (L.dailyLossSol !== undefined && -s.dayPnlSol >= L.dailyLossSol - 1e-12)
|
|
517
|
+
return { refused: 'real-daily-loss', detail: `today's realized loss ${r6(-s.dayPnlSol)} SOL reached dailyLossSol ${L.dailyLossSol}: new buys resume tomorrow` };
|
|
518
|
+
if (s.pausedUntil && now < s.pausedUntil)
|
|
519
|
+
return { refused: 'real-loss-streak', detail: `${L.lossStreak ?? '?'} losing closes in a row: new buys paused until ${new Date(s.pausedUntil).toISOString().slice(11, 16)} UTC` };
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Every buy limit at the moment of the signal: the pauses, the counters, the floors, then the size clamps (per
|
|
524
|
+
* trade SOL/%, position, exposure, price impact) and the refusals (daily SOL, per coin, minimum). Pure: the caller
|
|
525
|
+
* persists with recordBuy. Open-position count and held-elsewhere stay in the executor (they need the holdings).
|
|
526
|
+
*/
|
|
527
|
+
export function checkBuy(L, s, c) {
|
|
528
|
+
const no = (refused, detail) => ({ ok: false, refused, detail });
|
|
529
|
+
const p = buyPause(L, s, c.now);
|
|
530
|
+
if (p)
|
|
531
|
+
return no(p.refused, p.detail);
|
|
532
|
+
if (L.buysPerDay !== undefined && s.buys >= L.buysPerDay)
|
|
533
|
+
return no('real-buys-per-day', `${s.buys} buys today, buysPerDay ${L.buysPerDay}`);
|
|
534
|
+
if (L.maxBuysPerMinute !== undefined && s.recentBuys.filter((t) => c.now - t < 60_000).length >= L.maxBuysPerMinute)
|
|
535
|
+
return no('real-rate-limit', `maxBuysPerMinute ${L.maxBuysPerMinute}`);
|
|
536
|
+
const coin = s.perCoin[c.mint] ?? { buys: 0, sol: 0 };
|
|
537
|
+
if (L.perCoinBuysPerDay !== undefined && coin.buys >= L.perCoinBuysPerDay)
|
|
538
|
+
return no('real-per-coin-buys', `${coin.buys} buys on this coin today, perCoinBuysPerDay ${L.perCoinBuysPerDay}`);
|
|
539
|
+
if (L.minLiquidityUsd !== undefined) {
|
|
540
|
+
if (c.liqUsd === null || !Number.isFinite(c.liqUsd))
|
|
541
|
+
return no('real-liquidity-unknown', 'minLiquidityUsd is set and the pool liquidity is unknown');
|
|
542
|
+
if (c.liqUsd < L.minLiquidityUsd)
|
|
543
|
+
return no('real-min-liquidity', `liquidity $${Math.round(c.liqUsd)} < minLiquidityUsd ${L.minLiquidityUsd}`);
|
|
544
|
+
}
|
|
545
|
+
if (L.minMcapUsd !== undefined) {
|
|
546
|
+
if (c.mcapUsd === null || !Number.isFinite(c.mcapUsd))
|
|
547
|
+
return no('real-mcap-unknown', 'minMcapUsd is set and the market cap is unknown');
|
|
548
|
+
if (c.mcapUsd < L.minMcapUsd)
|
|
549
|
+
return no('real-min-mcap', `mcap $${Math.round(c.mcapUsd)} < minMcapUsd ${L.minMcapUsd}`);
|
|
550
|
+
}
|
|
551
|
+
const clamps = [];
|
|
552
|
+
let usd = c.usd;
|
|
553
|
+
const clamp = (cap, rule) => { if (usd > cap + 1e-9) {
|
|
554
|
+
usd = Math.max(0, cap);
|
|
555
|
+
clamps.push(rule);
|
|
556
|
+
} };
|
|
557
|
+
const bookUsd = L.bookSol !== undefined ? L.bookSol * c.solUsd : c.bookUsd;
|
|
558
|
+
if (L.perTradeSol !== undefined)
|
|
559
|
+
clamp(L.perTradeSol * c.solUsd, 'perTradeSol');
|
|
560
|
+
if (L.perTradePct !== undefined)
|
|
561
|
+
clamp((L.perTradePct / 100) * bookUsd, 'perTradePct');
|
|
562
|
+
if (L.maxPositionSol !== undefined)
|
|
563
|
+
clamp(L.maxPositionSol * c.solUsd - c.positionCostUsd, 'maxPositionSol');
|
|
564
|
+
if (L.maxExposurePct !== undefined)
|
|
565
|
+
clamp((L.maxExposurePct / 100) * bookUsd - c.openCostUsd, 'maxExposurePct');
|
|
566
|
+
let impactPct = null;
|
|
567
|
+
if (L.maxPriceImpactPct !== undefined) {
|
|
568
|
+
if (c.liqUsd === null || !(c.liqUsd > 0))
|
|
569
|
+
return no('real-impact-unknown', 'maxPriceImpactPct is set and the pool liquidity is unknown');
|
|
570
|
+
// The engine's constant-product model: the quote side is half the pool's liquidity and the impact is size / side.
|
|
571
|
+
const side = c.liqUsd / 2;
|
|
572
|
+
clamp((L.maxPriceImpactPct / 100) * side, 'maxPriceImpactPct');
|
|
573
|
+
impactPct = Math.round((usd / side) * 10_000) / 100;
|
|
574
|
+
}
|
|
575
|
+
if (!(usd > 1e-9))
|
|
576
|
+
return no(clamps.includes('maxExposurePct') ? 'real-max-exposure' : clamps.includes('maxPositionSol') ? 'real-max-position' : 'real-size-zero', `nothing left under ${clamps.join(', ') || 'the limits'}`);
|
|
577
|
+
const sol = usd / c.solUsd;
|
|
578
|
+
if (L.dailySol !== undefined && s.buySol + sol > L.dailySol + 1e-9)
|
|
579
|
+
return no('real-daily-cap', `${r6(s.buySol)} + ${r6(sol)} SOL > dailySol ${L.dailySol}`);
|
|
580
|
+
if (L.perCoinSolPerDay !== undefined && coin.sol + sol > L.perCoinSolPerDay + 1e-9)
|
|
581
|
+
return no('real-per-coin-sol', `${r6(coin.sol)} + ${r6(sol)} SOL on this coin > perCoinSolPerDay ${L.perCoinSolPerDay}`);
|
|
582
|
+
if (L.minPositionSol !== undefined && sol < L.minPositionSol - 1e-12)
|
|
583
|
+
return no('real-below-min-position', `${r6(sol)} SOL < minPositionSol ${L.minPositionSol}`);
|
|
584
|
+
return { ok: true, usd, clamps, impactPct };
|
|
585
|
+
}
|
|
586
|
+
export function recordBuy(s, mint, sol, now) {
|
|
587
|
+
s.buys += 1;
|
|
588
|
+
s.buySol = r6(s.buySol + sol);
|
|
589
|
+
const coin = s.perCoin[mint] ?? { buys: 0, sol: 0 };
|
|
590
|
+
s.perCoin[mint] = { buys: coin.buys + 1, sol: r6(coin.sol + sol) };
|
|
591
|
+
s.recentBuys.push(now);
|
|
592
|
+
}
|
|
593
|
+
/** A sell may wait only for the optional sell rate (sells are never capped otherwise). */
|
|
594
|
+
export function checkSell(L, s, now) {
|
|
595
|
+
if (L.maxSellsPerMinute !== undefined && s.recentSells.filter((t) => now - t < 60_000).length >= L.maxSellsPerMinute)
|
|
596
|
+
return { ok: false, refused: 'real-sell-rate', detail: `maxSellsPerMinute ${L.maxSellsPerMinute}: the sell waits for the next tick` };
|
|
597
|
+
return { ok: true };
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* A sell's realized PnL (SOL) into the day, the running total, the loss streak (on a full close) and the drawdown
|
|
601
|
+
* breaker. Returns what tripped, for the notification.
|
|
602
|
+
*/
|
|
603
|
+
export function recordSell(L, s, o) {
|
|
604
|
+
const tripped = [];
|
|
605
|
+
s.recentSells.push(o.now);
|
|
606
|
+
const wasDayLoss = L.dailyLossSol !== undefined && -s.dayPnlSol >= L.dailyLossSol;
|
|
607
|
+
s.dayPnlSol = r6(s.dayPnlSol + o.pnlSol);
|
|
608
|
+
s.cumPnlSol = r6(s.cumPnlSol + o.pnlSol);
|
|
609
|
+
s.peakCumPnlSol = Math.max(s.peakCumPnlSol, s.cumPnlSol);
|
|
610
|
+
if (!wasDayLoss && L.dailyLossSol !== undefined && -s.dayPnlSol >= L.dailyLossSol)
|
|
611
|
+
tripped.push(`daily loss limit reached (${r6(-s.dayPnlSol)} SOL): no new buy until tomorrow`);
|
|
612
|
+
if (o.closedPnlSol !== null) {
|
|
613
|
+
s.lossStreak = o.closedPnlSol < 0 ? s.lossStreak + 1 : 0;
|
|
614
|
+
if (L.lossStreak !== undefined && s.lossStreak >= L.lossStreak) {
|
|
615
|
+
const min = L.lossStreakPauseMin ?? 60;
|
|
616
|
+
s.pausedUntil = o.now + min * 60_000;
|
|
617
|
+
s.lossStreak = 0;
|
|
618
|
+
tripped.push(`${L.lossStreak} losing closes in a row: new buys paused ${min} min`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const dd = r6(s.peakCumPnlSol - s.cumPnlSol);
|
|
622
|
+
if (!s.breaker && L.maxDrawdownSol !== undefined && dd >= L.maxDrawdownSol) {
|
|
623
|
+
s.breaker = { at: o.now, drawdownSol: dd };
|
|
624
|
+
tripped.push(`drawdown breaker tripped (${dd} SOL below the peak): new buys stop until you reset it`);
|
|
625
|
+
}
|
|
626
|
+
return tripped;
|
|
627
|
+
}
|
|
628
|
+
/** `dexterm autotrade limits breaker reset` (typed): the breaker off, the peak moved to now. Journaled. */
|
|
629
|
+
export function resetBreaker(home, o) {
|
|
630
|
+
if (!o.confirmed)
|
|
631
|
+
return { ok: false, why: 'resetting the drawdown breaker needs the typed confirmation on a real terminal' };
|
|
632
|
+
const now = o.now ?? Date.now();
|
|
633
|
+
const s = loadRiskState(home, now);
|
|
634
|
+
const was = s.breaker;
|
|
635
|
+
s.breaker = null;
|
|
636
|
+
s.peakCumPnlSol = s.cumPnlSol;
|
|
637
|
+
s.pausedUntil = null;
|
|
638
|
+
s.lossStreak = 0;
|
|
639
|
+
saveRiskState(home, s);
|
|
640
|
+
journal(home, { t: now, action: 'breaker-reset', by: 'local-terminal', data: { was } });
|
|
641
|
+
return { ok: true, was };
|
|
642
|
+
}
|
|
643
|
+
// ───────────────────────────── presenting them ─────────────────────────────
|
|
644
|
+
export function fmtLimit(k, v) {
|
|
645
|
+
if (v === undefined || v === null)
|
|
646
|
+
return 'off';
|
|
647
|
+
return `${v} ${FIELD.get(k).unit}`;
|
|
648
|
+
}
|
|
649
|
+
/** Lines for `limits show` / the arming panel / the Q screen: effective value + where it came from. */
|
|
650
|
+
export function limitsLines(e, o = {}) {
|
|
651
|
+
const out = [];
|
|
652
|
+
for (const g of ['sizing', 'risk', 'sells', 'executor']) {
|
|
653
|
+
const rows = LIMIT_FIELDS.filter((f) => f.group === g && (o.all || e.values[f.key] !== undefined));
|
|
654
|
+
if (!rows.length)
|
|
655
|
+
continue;
|
|
656
|
+
out.push(`${g}:`);
|
|
657
|
+
for (const f of rows) {
|
|
658
|
+
const k = f.key;
|
|
659
|
+
out.push(` ${k.padEnd(19)} ${fmtLimit(k, e.values[k]).padEnd(18)} ${e.from[k] ? `(${e.from[k] === 'default' ? 'placeholder default' : e.from[k]})` : ''}`.trimEnd());
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
/** The one-line summary (status, run banner, Telegram). */
|
|
665
|
+
export function limitsSummary(v) {
|
|
666
|
+
const p = [];
|
|
667
|
+
if (v.perTradeSol !== undefined)
|
|
668
|
+
p.push(`${v.perTradeSol} SOL/trade`);
|
|
669
|
+
if (v.perTradePct !== undefined)
|
|
670
|
+
p.push(`${v.perTradePct}% of book/trade`);
|
|
671
|
+
if (v.dailySol !== undefined)
|
|
672
|
+
p.push(`${v.dailySol} SOL/day`);
|
|
673
|
+
if (v.buysPerDay !== undefined)
|
|
674
|
+
p.push(`${v.buysPerDay} buys/day`);
|
|
675
|
+
if (v.maxOpenPositions !== undefined)
|
|
676
|
+
p.push(`${v.maxOpenPositions} open`);
|
|
677
|
+
if (v.dailyLossSol !== undefined)
|
|
678
|
+
p.push(`stop at -${v.dailyLossSol} SOL/day`);
|
|
679
|
+
if (v.executorSilenceMin !== undefined)
|
|
680
|
+
p.push(`pause if the executor is silent ${v.executorSilenceMin} min`);
|
|
681
|
+
return p.join(' · ') || 'no limits on';
|
|
682
|
+
}
|