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
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// never sleep (the source paces itself) and replies land when they land.
|
|
13
13
|
//
|
|
14
14
|
// Every step lands in the hash-chained run ledger (ledger.ts) with the exact inputs (the tick, the message text, the
|
|
15
|
-
// raw reply, the clamp). Paper
|
|
15
|
+
// raw reply, the clamp). Paper, or real (task-1645: the paper engine plus a trade-fire signal per fill, through the
|
|
16
|
+
// real-mode hook only); any other mode is refused before the first tick. The
|
|
16
17
|
// sessions' day caps and allowance guard live in SessionGate (sessions.ts); with none given, the strategy's own
|
|
17
18
|
// limits apply; the gate counts real usage per wall-clock day, so a fast --replay over many tape days never resets
|
|
18
19
|
// the $/day cap. The host part at the end of this file (lock, rendezvous, heartbeat) is what the CLI uses.
|
|
@@ -33,14 +34,24 @@ export function simExecutor(x) {
|
|
|
33
34
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
34
35
|
const REVIEW = ['time_review', 'time_tighten'];
|
|
35
36
|
const INFO_ONLY = ['mechanical_exit', 'stuck'];
|
|
37
|
+
/** Session ends that fail the same way every time: no new session for the rest of the run. */
|
|
38
|
+
const GUARD_REASONS = new Set(['account-mismatch', 'not-tool-less', 'account-unavailable']);
|
|
36
39
|
async function* fromArray(ticks) { for (const k of ticks)
|
|
37
40
|
yield k; }
|
|
38
41
|
export async function runAutotrade(o) {
|
|
39
42
|
const s = o.strategy;
|
|
40
43
|
// task-1571: autotrade runs on paper only. dexterm holds no key and places no live order; live trading happens in the
|
|
41
44
|
// Telegram trading bot of the user's choice.
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// task-1645: mode "real" = the same paper engine, plus a LIVE TRADE FIRE signal per fill for the user's own executor;
|
|
46
|
+
// it needs the real-mode hook (armed on the user's terminal, checked by the host) and never runs without it.
|
|
47
|
+
if (s.mode === 'real' && !o.real)
|
|
48
|
+
throw new Error(`strategy ${s.id} is mode "real": it runs only through the real-mode host (dexterm autotrade run ${s.id} --real, once armed on your own terminal)`);
|
|
49
|
+
if (s.mode !== 'paper' && s.mode !== 'real')
|
|
50
|
+
throw new Error(`strategy ${s.id} is mode "${s.mode}": autotrade runs paper only (dexterm holds no key and places no live order); mode real runs only through the real-mode host`);
|
|
51
|
+
if (o.real && s.mode !== 'real')
|
|
52
|
+
throw new Error(`strategy ${s.id} is mode "${s.mode}": the real-mode hook is for a strategy in mode "real" only`);
|
|
53
|
+
if (o.real && (!o.executor || o.executor !== o.real))
|
|
54
|
+
throw new Error('real mode: the executor must be the real-mode signal executor itself');
|
|
44
55
|
if (!o.tape && !o.source)
|
|
45
56
|
throw new Error('runAutotrade needs a tape or a live source');
|
|
46
57
|
const ledger = o.ledger ?? new Ledger();
|
|
@@ -86,31 +97,131 @@ export async function runAutotrade(o) {
|
|
|
86
97
|
const pumps = new Set();
|
|
87
98
|
/** Proposals waiting for an answer, by mint (task-1527). */
|
|
88
99
|
const approvalsOpen = new Map();
|
|
89
|
-
ledger.add(t0, 'run_start', { strategy: s.id, version: s.version, strategyHash: s.hash, mode: s.mode, ...(s.highRisk ? { highRisk: true } : {}), tape: o.tape?.source ?? o.source.name, ticks: tapeTicks?.length ?? null, decide: decideAuto ? 'auto' : s.entry.decide, ...(o.approvals ? { approvals: o.approvals.required(s) ? 'required' : 'when-asked' } : {}), pacing: pacing.mode, executor: exec.name, caps: capsView(gate), ...(o.account ? { account: o.account.id } : {}) });
|
|
100
|
+
ledger.add(t0, 'run_start', { strategy: s.id, version: s.version, strategyHash: s.hash, mode: s.mode, ...(o.real ? { tag: 'real-signal', accountId: o.real.accountId } : {}), ...(s.highRisk ? { highRisk: true } : {}), tape: o.tape?.source ?? o.source.name, ticks: tapeTicks?.length ?? null, decide: decideAuto ? 'auto' : s.entry.decide, ...(o.approvals ? { approvals: o.approvals.required(s) ? 'required' : 'when-asked' } : {}), pacing: pacing.mode, executor: exec.name, caps: capsView(gate), ...(o.account ? { account: o.account.id } : {}) });
|
|
101
|
+
// task-1645 r2: a real run takes over the real positions an earlier run of this strategy left open (held.json),
|
|
102
|
+
// so their exits keep firing. A profile the strategy no longer has falls back to its default profile.
|
|
103
|
+
for (const p of o.real?.inherited() ?? []) {
|
|
104
|
+
const from = p.profileId;
|
|
105
|
+
if (!s.exits.profiles[p.profileId])
|
|
106
|
+
p.profileId = s.exits.default;
|
|
107
|
+
if (!book.adopt(p))
|
|
108
|
+
continue;
|
|
109
|
+
ledger.add(t0, 'position_adopted', { tag: 'real-signal', positionId: p.id, mint: p.mint, symbol: p.symbol, ...(from !== p.profileId ? { profileFrom: from } : {}), position: { ...p, fills: p.fills } });
|
|
110
|
+
say(`CARRIED ${p.symbol} (${p.mint.slice(0, 4)}…): real position from an earlier run, ${p.profileId}, cost $${(p.costUsd - p.realizedUsd).toFixed(2)} still open; its exits keep running`);
|
|
111
|
+
}
|
|
112
|
+
// 0.2.0 card B: positions a crashed run left open. A profile the (re-read) strategy no longer has falls back to
|
|
113
|
+
// its default profile, so every adopted position has exits.
|
|
114
|
+
for (const p of o.adopt ?? []) {
|
|
115
|
+
const from = p.profileId;
|
|
116
|
+
if (!s.exits.profiles[p.profileId])
|
|
117
|
+
p.profileId = s.exits.default;
|
|
118
|
+
if (!book.adopt(p))
|
|
119
|
+
continue;
|
|
120
|
+
ledger.add(t0, 'position_adopted', { positionId: p.id, mint: p.mint, symbol: p.symbol, ...(from !== p.profileId ? { profileFrom: from } : {}), position: { ...p, fills: p.fills } });
|
|
121
|
+
say(`RESUMED ${p.symbol} (${p.mint.slice(0, 4)}…): ${p.profileId}, cost $${(p.costUsd - p.realizedUsd).toFixed(2)} still open; its exits keep running`);
|
|
122
|
+
}
|
|
90
123
|
const count = (rule) => { refusals[rule] = (refusals[rule] ?? 0) + 1; };
|
|
91
124
|
const refuse = (rule, data) => { count(rule); ledger.add(now, 'refused', { rule, ...data }); };
|
|
92
125
|
// A vet that throws or hangs is a check that could not run: the strategy's `unknown` decides (reject by default).
|
|
93
|
-
const vetCoin = async (k) => {
|
|
126
|
+
const vetCoin = async (k, ctx) => {
|
|
94
127
|
let r;
|
|
95
128
|
try {
|
|
96
|
-
r = await o.vet(k);
|
|
129
|
+
r = await o.vet(k, ctx);
|
|
97
130
|
}
|
|
98
131
|
catch (e) {
|
|
99
132
|
r = { ok: false, rule: 'vet-error', detail: e.message.slice(0, 160) };
|
|
100
133
|
}
|
|
101
134
|
if (r.ok) {
|
|
102
|
-
|
|
135
|
+
if (!r.recheck)
|
|
136
|
+
vetted.add(k.mint);
|
|
103
137
|
if (r.note)
|
|
104
|
-
ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200) });
|
|
138
|
+
ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200), ...(r.sizeCapPct !== undefined ? { sizeCapPct: r.sizeCapPct } : {}) });
|
|
105
139
|
}
|
|
106
140
|
return r;
|
|
107
141
|
};
|
|
142
|
+
const vetCtx = (k, sizeUsd, h) => ({ history: h, sizeUsd, exitFloorUsd: x.minExitLiquidityUsd, maxTop10Pct: s.entry.safety.maxTop10Pct, maxDevPct: s.entry.safety.maxDevPct, unknownHolders: s.entry.safety.unknownHolders });
|
|
143
|
+
const drainGuard = s.entry.safety.rugproof?.drainGuard ?? null;
|
|
144
|
+
const stuckSince = new Map();
|
|
145
|
+
/** The liquidity read each position was bought at (the drain guard's base). */
|
|
146
|
+
const entryLiq = new Map();
|
|
147
|
+
// Paper only: a real-signal bag is still in the user's wallet, so it keeps its exit signals and carries to the next
|
|
148
|
+
// real run (validateStrategy also refuses the option outside mode paper).
|
|
149
|
+
const writeOffMin = !o.real && s.mode === 'paper' ? x.stuckWriteOffMin ?? null : null;
|
|
150
|
+
/** Positions the pool probe saw drained (no feed-priced retry while it still says so). */
|
|
151
|
+
const drained = new Set();
|
|
152
|
+
/**
|
|
153
|
+
* task-1748, every step, for every open position:
|
|
154
|
+
* - the drain guard: an exit verdict from the pool poller (SOL vault / Jupiter, <= 10 s apart), or this coin's own
|
|
155
|
+
* feed liquidity down more than drainGuard.liqDropPct from the entry read, sells the whole position at once. The
|
|
156
|
+
* sale is priced on the liquidity the guard saw (0 when the route is gone), not on a lagging feed row: an
|
|
157
|
+
* emptied pool leaves nothing to sell into, the bag goes stuck and the write-off below closes it.
|
|
158
|
+
* - execution.stuckWriteOffMin (paper): a bag stuck that long is closed at its realizable value (sells already
|
|
159
|
+
* made kept, the rest 0), with a written_off event and a trade_closed line, so crash recovery never adopts it.
|
|
160
|
+
*/
|
|
161
|
+
const sweepOpen = async (tick) => {
|
|
162
|
+
for (const p of [...book.open.values()]) {
|
|
163
|
+
if (p.status !== 'open')
|
|
164
|
+
continue;
|
|
165
|
+
const k = latest.get(p.mint) ?? p.lastTick;
|
|
166
|
+
if (drainGuard && !p.stuck) {
|
|
167
|
+
const v = o.drain?.verdict(p.id) ?? null;
|
|
168
|
+
const base = entryLiq.has(p.id) ? entryLiq.get(p.id) ?? null : (p.liqWindow[0]?.liq || null);
|
|
169
|
+
const own = tick && tick.mint === p.mint && base && tick.liq !== null && (1 - tick.liq / base) * 100 > drainGuard.liqDropPct
|
|
170
|
+
? { reason: 'drain-liquidity', detail: `liquidity $${Math.round(base)} -> $${Math.round(tick.liq)} since entry (-${((1 - tick.liq / base) * 100).toFixed(1)}% > ${drainGuard.liqDropPct}%)`, liqUsd: tick.liq } : null;
|
|
171
|
+
const hit = v ?? own;
|
|
172
|
+
if (hit) {
|
|
173
|
+
ledger.add(now, 'drain_guard', { positionId: p.id, mint: p.mint, reason: hit.reason, detail: hit.detail, liqUsd: hit.liqUsd, source: v ? 'pool-probe' : 'feed' });
|
|
174
|
+
say(`DRAIN GUARD ${p.symbol}: ${hit.detail}`);
|
|
175
|
+
await sellPosition(p, { ...k, t: now, liq: hit.liqUsd ?? 0 }, p.tokens, hit.reason, hit.detail);
|
|
176
|
+
// The probe saw the pool emptied or the route gone: the lagging feed must not "sell" the bag at its old
|
|
177
|
+
// liquidity. While the probe still says so, the monitor's stuck retry is off for this position.
|
|
178
|
+
if (v && p.status === 'open' && p.stuck)
|
|
179
|
+
drained.add(p.id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const since = stuckSince.get(p.id);
|
|
183
|
+
if (writeOffMin !== null && p.status === 'open' && p.stuck && since !== undefined && now - since >= writeOffMin * 60e3) {
|
|
184
|
+
const ct = book.close(p, now, 'written-off', true);
|
|
185
|
+
o.drain?.unwatch(p.id);
|
|
186
|
+
stuckSince.delete(p.id);
|
|
187
|
+
ledger.add(now, 'written_off', { positionId: p.id, mint: p.mint, stuckMin: Math.round((now - since) / 6e3) / 10, realizedUsd: ct.proceedsUsd, costUsd: ct.costUsd, why: `stuck ${writeOffMin} min under the exit floor: closed at realizable value (the rest counts 0)` });
|
|
188
|
+
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, writtenOff: true });
|
|
189
|
+
say(`WRITTEN OFF ${p.symbol}: stuck ${writeOffMin} min, ${ct.pnlPct}% realized`);
|
|
190
|
+
}
|
|
191
|
+
if (p.status === 'closed') {
|
|
192
|
+
const c = byPosition.get(p.id);
|
|
193
|
+
if (c) {
|
|
194
|
+
if (c.inflight)
|
|
195
|
+
await c.inflight;
|
|
196
|
+
await closeCandidate(c, 'position-closed');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
108
201
|
const liveCandidateSessions = () => [...cands.values()].filter((q) => q.session && !q.positionId).length;
|
|
109
202
|
const everyLive = () => [...new Set([...cands.values(), ...byPosition.values()])];
|
|
110
203
|
const newCand = (k, matched, session, kind, positionId) => ({
|
|
111
204
|
mint: k.mint, symbol: k.symbol, since: now, lastMatch: now, matched, session, sessionKind: kind, lastSent: view(k), lastSentT: now, seq: 0,
|
|
112
205
|
inflight: null, pendingText: [], notes: [], closing: false, positionId, awaitingReview: null, oneShot: kind === 'position', costSeen: 0, turns: 0, lastDecision: null,
|
|
113
206
|
});
|
|
207
|
+
/**
|
|
208
|
+
* Why the session's process ended on its own (null while alive). A session that proved the account or its tool list
|
|
209
|
+
* wrong (or had no account) blocks new sessions for the run; the guard is logged once, by the session that set it.
|
|
210
|
+
* Checked on every path that learns of a death: a null reply AND a close (a candidate's TTL can close a session that
|
|
211
|
+
* already died, before its null reply is read; task-1712).
|
|
212
|
+
*/
|
|
213
|
+
let guardLogged = false;
|
|
214
|
+
const sessionDied = (c) => {
|
|
215
|
+
const died = c.session?.endReason?.() ?? null;
|
|
216
|
+
if (c.session && died && GUARD_REASONS.has(died)) {
|
|
217
|
+
if (!guardLogged && (gate.blocked === null || gate.blockedBy === c.session.id)) {
|
|
218
|
+
guardLogged = true;
|
|
219
|
+
ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
|
|
220
|
+
}
|
|
221
|
+
gate.block(died, c.session.id);
|
|
222
|
+
}
|
|
223
|
+
return died;
|
|
224
|
+
};
|
|
114
225
|
const closeCandidate = async (c, reason) => {
|
|
115
226
|
if (c.closing)
|
|
116
227
|
return;
|
|
@@ -132,8 +243,11 @@ export async function runAutotrade(o) {
|
|
|
132
243
|
c.costSeen = st.costUsd;
|
|
133
244
|
}
|
|
134
245
|
sessionCost += st.costUsd;
|
|
135
|
-
|
|
136
|
-
|
|
246
|
+
// A session that had already failed its proof ended for that reason, whatever closed its candidate.
|
|
247
|
+
const died = sessionDied(c);
|
|
248
|
+
const why = died && GUARD_REASONS.has(died) ? died : reason;
|
|
249
|
+
ledger.add(now, 'session_end', { sessionId: c.session.id, sessionKind: c.sessionKind, mint: c.mint, reason: why, turns: st.turns, costUsd: st.costUsd });
|
|
250
|
+
say(`session ${c.session.id} ended (${why}) · ${st.turns} turns · $${st.costUsd.toFixed(4)}`);
|
|
137
251
|
}
|
|
138
252
|
};
|
|
139
253
|
const sellPosition = async (p, k, tokens, reason, detail) => {
|
|
@@ -143,21 +257,37 @@ export async function runAutotrade(o) {
|
|
|
143
257
|
if ('refused' in f) {
|
|
144
258
|
if (!p.stuck) {
|
|
145
259
|
p.stuck = true;
|
|
260
|
+
stuckSince.set(p.id, now);
|
|
146
261
|
ledger.add(now, 'stuck', { positionId: p.id, mint: p.mint, why: f.refused, reason });
|
|
147
262
|
say(`STUCK ${p.symbol}: cannot sell (${f.refused})`);
|
|
148
263
|
}
|
|
149
264
|
return;
|
|
150
265
|
}
|
|
151
266
|
p.stuck = false;
|
|
152
|
-
|
|
267
|
+
stuckSince.delete(p.id);
|
|
268
|
+
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), detail, ...sigTag(f) });
|
|
153
269
|
say(`SELL ${p.symbol} ${reason}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)} (impact ${f.impactPct}%) — ${detail}`);
|
|
154
270
|
const ct = book.sell(p, f);
|
|
271
|
+
if (ct)
|
|
272
|
+
o.drain?.unwatch(p.id);
|
|
155
273
|
if (ct) {
|
|
156
274
|
// task-1530: the price extremes the monitor saw while held (the trade ledger's MFE / MAE)
|
|
157
|
-
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null });
|
|
275
|
+
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, ...(o.real ? { tag: 'real-signal' } : {}) });
|
|
158
276
|
say(`CLOSED ${ct.symbol}: ${ct.pnlUsd >= 0 ? '+' : ''}$${ct.pnlUsd.toFixed(2)} (${ct.pnlPct}%) by ${ct.exitReason}`);
|
|
159
277
|
}
|
|
160
278
|
};
|
|
279
|
+
/** task-1748: a young or partly verified pool takes at most the vet's sizeCapPct of the strategy's pick. */
|
|
280
|
+
const capYoungPool = (r, c) => {
|
|
281
|
+
if (!r.ok || c.sizeCapPct === undefined)
|
|
282
|
+
return r;
|
|
283
|
+
const capUsd = Math.round(r.usd * c.sizeCapPct) / 100;
|
|
284
|
+
if (capUsd >= r.usd)
|
|
285
|
+
return r;
|
|
286
|
+
if (capUsd < s.sizing.minUsd)
|
|
287
|
+
return { ok: false, refused: 'below-min-after-rugproof-young-pool', clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
|
|
288
|
+
const f = r.usd ? capUsd / r.usd : 1;
|
|
289
|
+
return { ...r, usd: capUsd, firstTrancheUsd: Math.round(r.firstTrancheUsd * f * 100) / 100, clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
|
|
290
|
+
};
|
|
161
291
|
/** Apply a validated decision at the current virtual time. */
|
|
162
292
|
const apply = async (c, d) => {
|
|
163
293
|
const k = latest.get(c.mint);
|
|
@@ -182,7 +312,7 @@ export async function runAutotrade(o) {
|
|
|
182
312
|
return;
|
|
183
313
|
}
|
|
184
314
|
const req = { profile: d.profile, sizePct: d.sizePct, sizeUsd: d.sizeUsd, confidence: d.confidence };
|
|
185
|
-
const r = clampEntry(book, k, req, killed);
|
|
315
|
+
const r = capYoungPool(clampEntry(book, k, req, killed), c);
|
|
186
316
|
ledger.add(now, 'clamp', { mint: c.mint, sessionId: c.session?.id ?? null, request: req, result: r.ok ? { ok: true, usd: r.usd, firstTrancheUsd: r.firstTrancheUsd, profile: r.profile.id, basis: r.basis, clamps: r.clamps } : r });
|
|
187
317
|
for (const cl of r.clamps)
|
|
188
318
|
clampCount[cl.rule] = (clampCount[cl.rule] ?? 0) + 1;
|
|
@@ -325,14 +455,9 @@ export async function runAutotrade(o) {
|
|
|
325
455
|
c.costSeen = Math.max(c.costSeen, r.costUsd);
|
|
326
456
|
}
|
|
327
457
|
if (!r || !r.decision.ok) {
|
|
328
|
-
const died = c
|
|
458
|
+
const died = sessionDied(c);
|
|
329
459
|
if (!r)
|
|
330
460
|
ledger.add(now, 'session_timeout', { sessionId: c.session.id, mint: c.mint, ended: died });
|
|
331
|
-
if (died === 'account-mismatch' || died === 'not-tool-less' || died === 'account-unavailable') {
|
|
332
|
-
if (!gate.blocked)
|
|
333
|
-
ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
|
|
334
|
-
gate.block(died);
|
|
335
|
-
}
|
|
336
461
|
if (review)
|
|
337
462
|
await noAnswer(review, !r ? (died ?? 'timeout') : 'invalid-decision');
|
|
338
463
|
if (!r) {
|
|
@@ -436,16 +561,18 @@ export async function runAutotrade(o) {
|
|
|
436
561
|
f.reason = 'ladder-add';
|
|
437
562
|
const bookUsd = round4(book.bookEquity()); // task-1530: the book before the fill (the trade ledger's % of book)
|
|
438
563
|
book.addTranche(p, f);
|
|
439
|
-
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), bookUsd });
|
|
564
|
+
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), bookUsd, ...sigTag(f) });
|
|
440
565
|
say(`ADD ${k.symbol}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}`);
|
|
441
566
|
continue;
|
|
442
567
|
}
|
|
443
568
|
const prof = s.exits.profiles[q.profile];
|
|
444
569
|
const bookUsd = round4(book.bookEquity()); // task-1530: the book before the fill (the trade ledger's % of book)
|
|
445
570
|
const p = book.openPosition(k, f, prof, q.sessionId, q.trigger, q.ladder);
|
|
446
|
-
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, profile: prof.id, ...fillMcaps(fillView(f), k), bookUsd, ...(q.ctx?.proposalId ? { proposalId: q.ctx.proposalId } : {}) });
|
|
571
|
+
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, profile: prof.id, ...fillMcaps(fillView(f), k), bookUsd, ...(q.ctx?.proposalId ? { proposalId: q.ctx.proposalId } : {}), ...sigTag(f) });
|
|
447
572
|
say(`BUY ${k.symbol} [${prof.id}]: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}${f.partial ? ' (PARTIAL: impact cap)' : ''} · liq $${Math.round(k.liq ?? 0)}`);
|
|
448
573
|
const c = cands.get(q.mint);
|
|
574
|
+
entryLiq.set(p.id, k.liq ?? null);
|
|
575
|
+
o.drain?.watch({ positionId: p.id, mint: p.mint, entryLiqUsd: k.liq ?? null, poolAddress: c?.pool?.address ?? null, quoteVault: c?.pool?.quoteVault ?? null, tokens: () => p.tokens });
|
|
449
576
|
if (c) {
|
|
450
577
|
c.positionId = p.id;
|
|
451
578
|
byPosition.set(p.id, c);
|
|
@@ -525,18 +652,25 @@ export async function runAutotrade(o) {
|
|
|
525
652
|
sessions: live.map((c) => ({ id: c.session.id, kind: c.sessionKind, mint: c.mint, symbol: c.symbol, turns: c.turns, costUsd: round4(c.session.costUsd()), budgetUsd: gate.caps.budgetUsdPerSession, lastDecision: c.lastDecision })),
|
|
526
653
|
positions: [...book.open.values()].map((p) => {
|
|
527
654
|
const pnl = liquidationMark(p) + p.realizedUsd - p.costUsd;
|
|
528
|
-
return { mint: p.mint, symbol: p.symbol, profile: p.profileId, costUsd: round2(p.costUsd), pnlUsd: round2(pnl), pnlPct: p.costUsd ? Math.round((pnl / p.costUsd) * 1000) / 10 : 0, next: p.stuck ? 'stuck: retried every tick' : null, ...(p.costRecovered ? { freeBag: true } : {}) };
|
|
655
|
+
return { mint: p.mint, symbol: p.symbol, profile: p.profileId, costUsd: round2(p.costUsd), pnlUsd: round2(pnl), pnlPct: p.costUsd ? Math.round((pnl / p.costUsd) * 1000) / 10 : 0, next: p.stuck ? 'stuck: retried every tick' : null, ...(p.costRecovered ? { freeBag: true } : {}), ...(o.real ? { confirm: o.real.confirmOf(p.mint) ?? 'unconfirmed' } : {}) };
|
|
529
656
|
}),
|
|
530
657
|
ledgerTail: ledger.events.slice(-12).map((e) => ({ seq: e.seq, t: e.t, kind: e.kind, text: tailText(e) })),
|
|
531
658
|
ledgerHead: ledger.head(),
|
|
532
659
|
events: ledger.events.length,
|
|
660
|
+
...(o.real ? { real: o.real.status() } : {}),
|
|
533
661
|
};
|
|
534
662
|
};
|
|
535
663
|
const loop = tapeTicks ? fromArray(tapeTicks) : o.source.ticks;
|
|
536
664
|
for await (const k of loop) {
|
|
537
665
|
now = k.t;
|
|
538
666
|
const v = o.safety?.() ?? null;
|
|
539
|
-
|
|
667
|
+
// task-1645: receipts land in the ledger as data; the real-mode kill switch halts like the panic file.
|
|
668
|
+
const rs = o.real?.step() ?? null;
|
|
669
|
+
for (const r of rs?.receipts ?? []) {
|
|
670
|
+
const dv = r.divergence;
|
|
671
|
+
ledger.add(now, 'receipt', { tag: 'real-signal', nonce: r.nonce, signalId: r.signalId, status: r.status, fill: r.fill, reason: r.reason, executor: r.executor, ...(dv ? { divergence: dv } : {}) });
|
|
672
|
+
}
|
|
673
|
+
const haltWhy = v?.halt ?? (o.killSwitch?.() ? 'kill-switch' : null) ?? rs?.halt ?? null;
|
|
540
674
|
if (!killed && haltWhy) {
|
|
541
675
|
killed = true;
|
|
542
676
|
haltReason = haltWhy;
|
|
@@ -579,6 +713,7 @@ export async function runAutotrade(o) {
|
|
|
579
713
|
ledger.add(now, 'halt', { reason: book.state.halted });
|
|
580
714
|
o.approvals?.cancelAll(`halted:${book.state.halted}`);
|
|
581
715
|
}
|
|
716
|
+
await sweepOpen(k);
|
|
582
717
|
// Held coin → the monitor (mechanical first, then wakes).
|
|
583
718
|
const pos = book.open.get(k.mint);
|
|
584
719
|
const chatSell = o.approvals?.takeExit?.(k.mint) ?? null;
|
|
@@ -608,8 +743,15 @@ export async function runAutotrade(o) {
|
|
|
608
743
|
}
|
|
609
744
|
}
|
|
610
745
|
}
|
|
746
|
+
else if (pos && drained.has(pos.id) && o.drain?.verdict(pos.id)) {
|
|
747
|
+
// task-1748: still drained per the pool probe: nothing sells into an emptied pool (the write-off closes it).
|
|
748
|
+
}
|
|
611
749
|
else if (pos) {
|
|
612
|
-
|
|
750
|
+
drained.delete(pos.id);
|
|
751
|
+
const prof0 = s.exits.profiles[pos.profileId];
|
|
752
|
+
// task-1654: a real run's (or a paper scope's) maxHoldMinutes limit tightens the profile's holding cap (never loosens it).
|
|
753
|
+
const holdCap = o.real?.maxHoldMinutes?.() ?? o.maxHoldMinutes?.() ?? null;
|
|
754
|
+
const prof = holdCap !== null && (prof0.maxHoldMinutes == null || holdCap < prof0.maxHoldMinutes) ? { ...prof0, maxHoldMinutes: holdCap } : prof0;
|
|
613
755
|
const acts = monitorTick(pos, prof, k, { minExitLiquidityUsd: x.minExitLiquidityUsd, sellUsd: (tok) => { const f = simSell(k, tok, x, 'estimate'); return 'refused' in f ? 0 : f.usd; } });
|
|
614
756
|
const wakes = [];
|
|
615
757
|
for (const a of acts) {
|
|
@@ -666,18 +808,26 @@ export async function runAutotrade(o) {
|
|
|
666
808
|
if (sr.pass) {
|
|
667
809
|
// Cheap pre-check so a full book does not spawn sessions it could never act on.
|
|
668
810
|
const pre = clampEntry(book, k, {}, killed);
|
|
669
|
-
const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k) : null;
|
|
811
|
+
const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k, vetCtx(k, pre.usd, h)) : null;
|
|
670
812
|
if (!pre.ok) {
|
|
671
813
|
if (!['already-open'].includes(pre.refused))
|
|
672
814
|
count(`precheck:${pre.refused}`);
|
|
673
815
|
}
|
|
674
816
|
else if (v && !v.ok) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
817
|
+
// task-1748: a time gate (pool age, the second liquidity read) looks again later; any other refusal is for good.
|
|
818
|
+
const retry = v.retryAfterMs !== undefined;
|
|
819
|
+
refuse(v.rule, { mint: k.mint, symbol: k.symbol, stage: 'vet', detail: v.detail, ...(retry ? { retryAfterMs: v.retryAfterMs } : {}) });
|
|
820
|
+
skippedUntil.set(k.mint, retry ? now + v.retryAfterMs : Infinity);
|
|
821
|
+
say(`SKIP ${k.symbol} (${k.mint.slice(0, 4)}…): ${v.rule} (${v.detail})${retry ? `; again in ${Math.round(v.retryAfterMs / 1000)} s` : ''}`);
|
|
678
822
|
}
|
|
679
823
|
else if (decideAuto) {
|
|
680
824
|
const cand = newCand(k, sr.matched, null, 'candidate', null);
|
|
825
|
+
if (v?.ok) {
|
|
826
|
+
if (v.sizeCapPct !== undefined)
|
|
827
|
+
cand.sizeCapPct = v.sizeCapPct;
|
|
828
|
+
if (v.pool)
|
|
829
|
+
cand.pool = v.pool;
|
|
830
|
+
}
|
|
681
831
|
cands.set(k.mint, cand);
|
|
682
832
|
ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
|
|
683
833
|
await apply(cand, { action: 'enter', reason: 'auto' });
|
|
@@ -693,6 +843,12 @@ export async function runAutotrade(o) {
|
|
|
693
843
|
const session = o.sessions(sid, s, { kind: 'candidate', mint: k.mint });
|
|
694
844
|
gate.opened('candidate');
|
|
695
845
|
const cand = newCand(k, sr.matched, session, 'candidate', null);
|
|
846
|
+
if (v?.ok) {
|
|
847
|
+
if (v.sizeCapPct !== undefined)
|
|
848
|
+
cand.sizeCapPct = v.sizeCapPct;
|
|
849
|
+
if (v.pool)
|
|
850
|
+
cand.pool = v.pool;
|
|
851
|
+
}
|
|
696
852
|
cands.set(k.mint, cand);
|
|
697
853
|
ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
|
|
698
854
|
ledger.add(now, 'session_start', { sessionId: sid, sessionKind: 'candidate', agent: session.kind, mint: k.mint, model: sessionModel(session, s), budgetUsd: gate.caps.budgetUsdPerSession });
|
|
@@ -746,6 +902,14 @@ export async function runAutotrade(o) {
|
|
|
746
902
|
while (pumps.size)
|
|
747
903
|
await Promise.allSettled([...pumps]);
|
|
748
904
|
const openAtEnd = [...book.open.values()].map((p) => ({ mint: p.mint, symbol: p.symbol, markUsd: round2(liquidationMark(p)), costUsd: p.costUsd, stuck: p.stuck }));
|
|
905
|
+
if (o.real) {
|
|
906
|
+
// The dead-man's graceful half: the executor hears the run is over (its outstanding nonces may be cancelled).
|
|
907
|
+
const why = killed ? `halt:${haltReason ?? 'kill'}` : o.tape ? 'end-of-tape' : 'runner-stopped';
|
|
908
|
+
// r2: what is still open is carried (held.json): the next real run of this strategy adopts it and keeps its exits.
|
|
909
|
+
o.real.carry([...book.open.values()]);
|
|
910
|
+
o.real.sendHalted(why);
|
|
911
|
+
ledger.add(now, 'real_halted', { tag: 'real-signal', reason: why, openAtEnd: openAtEnd.length });
|
|
912
|
+
}
|
|
749
913
|
ledger.add(now, 'run_end', { trades: book.closed.length, openAtEnd: openAtEnd.length, cashUsd: book.state.cashUsd, markedEquityUsd: book.markedEquity(), sessionCostUsd: sessionCost, sessions: sessionsOpened, positionSessions: positionSessionsOpened, head: ledger.head() });
|
|
750
914
|
publishFocus();
|
|
751
915
|
if (o.onState)
|
|
@@ -772,6 +936,8 @@ export function exitPlanLine(p) {
|
|
|
772
936
|
return `${p.id}: ${parts.join(', ')}`;
|
|
773
937
|
}
|
|
774
938
|
const round4 = (v) => Math.round(v * 1e4) / 1e4;
|
|
939
|
+
/** task-1645: a fill that went out as a trade-fire signal: the ledger row is tagged and names the signal. */
|
|
940
|
+
const sigTag = (f) => { const x = f; const g = x.signal; return g ? { tag: 'real-signal', nonce: g.nonce, signalId: g.id, ...(x.limitClamps?.length ? { limitClamps: x.limitClamps } : {}) } : {}; };
|
|
775
941
|
const fillView = (f) => ({ side: f.side, usd: f.usd, tokens: Number(f.tokens.toPrecision(8)), price: f.price, avgPrice: f.avgPrice, impactPct: f.impactPct, partial: f.partial, reason: f.reason });
|
|
776
942
|
/** Infinity (no runner cap) is written as null so the ledger stays JSON. */
|
|
777
943
|
/** The model a session actually runs (a run's --model override wins over the strategy's), for the ledger. */
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// dexterm autotrade — the always-on service's guard aggregator (0.2.0 card B, AUTOTRADE-SPEC §7.3 / §7.4).
|
|
2
|
+
//
|
|
3
|
+
// Every 10 s (and on demand) the service folds what it can see into named guard states. Each state has ONE effect:
|
|
4
|
+
// stop the service stops the fleet (a soft stop: no new entries, sessions closed, exits keep running)
|
|
5
|
+
// pause-entries no new entries while it holds; lifts by itself (hysteresis where a value can flap)
|
|
6
|
+
// notice shown in `service status` and sent once as a notification; nothing pauses
|
|
7
|
+
// Open positions are never paused by any state: their exits keep running (the monitor is never paused, §7.4).
|
|
8
|
+
//
|
|
9
|
+
// state id effect from
|
|
10
|
+
// stop-file stop the service STOP marker (`service stop`, a plain `touch`)
|
|
11
|
+
// panic stop the panic file (`dexterm wallet panic`); the fleet halts itself too
|
|
12
|
+
// disk-low pause-entries free space under runner.json disk.minFreeGb (default 5 GB); +1 GB to lift
|
|
13
|
+
// load-high pause-entries 1-min load above load.pauseAboveLoad1m for load.sustainSec; lifts below resumeBelowLoad1m
|
|
14
|
+
// busy-process pause-entries a runner.json load.busyProcs pattern matches a running process (not ours)
|
|
15
|
+
// data-stale pause-entries no strategy has seen a market tick for dataStaleSec (default 300 s)
|
|
16
|
+
// runner-stale notice the fleet's heartbeat (runner-status.json) is older than 30 s
|
|
17
|
+
// allowance-high notice the model usage windows are above runner.json allowanceGuard (the fleet pauses sessions itself)
|
|
18
|
+
// day-cap notice the fleet's global day caps are used up (sessions resume when the day rolls)
|
|
19
|
+
// recorder-down notice the market recorder is installed but has written nothing for recorderStaleSec
|
|
20
|
+
// (default 15 min); not installed → notice with `dexterm record install`
|
|
21
|
+
// strategy-halted notice a strategy halted itself (daily loss / drawdown caps); one per strategy
|
|
22
|
+
//
|
|
23
|
+
// The guard is pure over its inputs (the service collects them); transitions come back so the caller can log one
|
|
24
|
+
// ledger line and send one notification per change, never one per tick.
|
|
25
|
+
export const DEFAULT_GUARD_CONFIG = {
|
|
26
|
+
minFreeGb: 5, pauseAboveLoad1m: 25, resumeBelowLoad1m: 20, sustainSec: 120, busyProcs: [],
|
|
27
|
+
dataStaleSec: 300, recorderStaleSec: 900, fiveHourMax: 0.6, sevenDayMax: 0.8,
|
|
28
|
+
};
|
|
29
|
+
/** runner.json → the guard's knobs (the stricter of the two never matters here: these only pause). */
|
|
30
|
+
export function guardConfigFrom(rc, over = {}) {
|
|
31
|
+
return {
|
|
32
|
+
...DEFAULT_GUARD_CONFIG,
|
|
33
|
+
...(rc?.load ? { pauseAboveLoad1m: rc.load.pauseAboveLoad1m, resumeBelowLoad1m: rc.load.resumeBelowLoad1m, sustainSec: rc.load.sustainSec, busyProcs: [...rc.load.busyProcs] } : {}),
|
|
34
|
+
...(rc?.disk ? { minFreeGb: rc.disk.minFreeGb } : {}),
|
|
35
|
+
...(rc?.allowanceGuard ? { fiveHourMax: rc.allowanceGuard.fiveHourMax, sevenDayMax: rc.allowanceGuard.sevenDayMax } : {}),
|
|
36
|
+
...over,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const gb = (n) => `${n.toFixed(1)} GB`;
|
|
40
|
+
export class ServiceGuard {
|
|
41
|
+
cfg;
|
|
42
|
+
active = new Map();
|
|
43
|
+
loadAboveSince = null;
|
|
44
|
+
loadPaused = false;
|
|
45
|
+
diskLow = false;
|
|
46
|
+
constructor(cfg = DEFAULT_GUARD_CONFIG) { this.cfg = cfg; }
|
|
47
|
+
evaluate(i) {
|
|
48
|
+
const c = this.cfg;
|
|
49
|
+
const want = new Map();
|
|
50
|
+
const on = (id, effect, detail) => { want.set(id, { effect, detail }); };
|
|
51
|
+
if (i.stopFile)
|
|
52
|
+
on('stop-file', 'stop', i.stopFile);
|
|
53
|
+
if (i.panic)
|
|
54
|
+
on('panic', 'stop', `the panic switch is engaged (${i.panic}); \`dexterm wallet resume\` lifts it`);
|
|
55
|
+
if (i.freeGb !== null) {
|
|
56
|
+
if (!this.diskLow && i.freeGb < c.minFreeGb)
|
|
57
|
+
this.diskLow = true;
|
|
58
|
+
else if (this.diskLow && i.freeGb >= c.minFreeGb + 1)
|
|
59
|
+
this.diskLow = false;
|
|
60
|
+
if (this.diskLow)
|
|
61
|
+
on('disk-low', 'pause-entries', `${gb(i.freeGb)} free, under the ${gb(c.minFreeGb)} floor (runner.json disk.minFreeGb); lifts at ${gb(c.minFreeGb + 1)}`);
|
|
62
|
+
}
|
|
63
|
+
if (i.load1m !== null) {
|
|
64
|
+
if (i.load1m > c.pauseAboveLoad1m) {
|
|
65
|
+
this.loadAboveSince ??= i.now;
|
|
66
|
+
if (i.now - this.loadAboveSince >= c.sustainSec * 1000)
|
|
67
|
+
this.loadPaused = true;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.loadAboveSince = null;
|
|
71
|
+
if (i.load1m < c.resumeBelowLoad1m)
|
|
72
|
+
this.loadPaused = false;
|
|
73
|
+
}
|
|
74
|
+
if (this.loadPaused)
|
|
75
|
+
on('load-high', 'pause-entries', `1-min load ${i.load1m.toFixed(1)} (pauses above ${c.pauseAboveLoad1m} for ${c.sustainSec} s, resumes below ${c.resumeBelowLoad1m})`);
|
|
76
|
+
}
|
|
77
|
+
if (i.busy.length)
|
|
78
|
+
on('busy-process', 'pause-entries', `a busy process is running (${i.busy.slice(0, 3).join(', ')})`);
|
|
79
|
+
if (i.upMs >= c.dataStaleSec * 1000 && (i.dataAgeMs === null || i.dataAgeMs > c.dataStaleSec * 1000)) {
|
|
80
|
+
on('data-stale', 'pause-entries', i.dataAgeMs === null ? `no market tick since the start (${Math.round(i.upMs / 1000)} s)` : `the newest market tick is ${Math.round(i.dataAgeMs / 1000)} s old (limit ${c.dataStaleSec} s)`);
|
|
81
|
+
}
|
|
82
|
+
if (i.runnerAgeMs !== null && i.runnerAgeMs > 30_000)
|
|
83
|
+
on('runner-stale', 'notice', `the strategies' heartbeat is ${Math.round(i.runnerAgeMs / 1000)} s old`);
|
|
84
|
+
const a = i.allowance;
|
|
85
|
+
if (a && ((a.fiveHour ?? 0) > c.fiveHourMax || (a.sevenDay ?? 0) > c.sevenDayMax)) {
|
|
86
|
+
on('allowance-high', 'notice', `model usage 5 h ${pctOf(a.fiveHour)} / 7 d ${pctOf(a.sevenDay)} (limits ${pctOf(c.fiveHourMax)} / ${pctOf(c.sevenDayMax)}): no new sessions until it falls`);
|
|
87
|
+
}
|
|
88
|
+
if (i.dayCap)
|
|
89
|
+
on('day-cap', 'notice', `${i.dayCap}: no new sessions until the day rolls`);
|
|
90
|
+
if (i.recorder) {
|
|
91
|
+
if (!i.recorder.installed)
|
|
92
|
+
on('recorder-down', 'notice', 'the market recorder is not installed: backtests and tuning see no new history (`dexterm record install`)');
|
|
93
|
+
else if (i.recorder.newestAgeMs === null || i.recorder.newestAgeMs > c.recorderStaleSec * 1000)
|
|
94
|
+
on('recorder-down', 'notice', `the market recorder has written nothing for ${i.recorder.newestAgeMs === null ? 'ever' : `${Math.round(i.recorder.newestAgeMs / 60_000)} min`} (\`dexterm record status\`)`);
|
|
95
|
+
}
|
|
96
|
+
for (const [id, why] of Object.entries(i.halted))
|
|
97
|
+
on(`strategy-halted:${id}`, 'notice', `strategy ${id} halted itself (${why}); its open positions keep their exits`);
|
|
98
|
+
const transitions = [];
|
|
99
|
+
for (const [id, w] of want) {
|
|
100
|
+
const cur = this.active.get(id);
|
|
101
|
+
if (!cur) {
|
|
102
|
+
this.active.set(id, { id, effect: w.effect, detail: w.detail, since: i.now });
|
|
103
|
+
transitions.push({ id, on: true, effect: w.effect, detail: w.detail });
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
cur.detail = w.detail;
|
|
107
|
+
}
|
|
108
|
+
for (const [id, cur] of [...this.active]) {
|
|
109
|
+
if (want.has(id))
|
|
110
|
+
continue;
|
|
111
|
+
this.active.delete(id);
|
|
112
|
+
transitions.push({ id, on: false, effect: cur.effect, detail: cur.detail });
|
|
113
|
+
}
|
|
114
|
+
const states = [...this.active.values()].map((s) => ({ ...s }));
|
|
115
|
+
const stop = states.find((s) => s.effect === 'stop')?.id ?? null;
|
|
116
|
+
const pause = states.find((s) => s.effect === 'pause-entries')?.id ?? null;
|
|
117
|
+
return { states, stop, pauseEntries: pause ? `guard:${pause}` : null, transitions };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const pctOf = (v) => (v === null ? '?' : `${Math.round(v * 100)}%`);
|
|
121
|
+
/** A one-line notification text for a guard transition (plain text, no paths, no secrets). */
|
|
122
|
+
export function guardNoticeText(t) {
|
|
123
|
+
const what = t.effect === 'stop' ? 'STOP' : t.effect === 'pause-entries' ? 'ENTRIES PAUSED' : 'NOTICE';
|
|
124
|
+
return t.on ? `autotrade service · ${what} · ${t.id}: ${t.detail}` : `autotrade service · cleared · ${t.id}${t.effect === 'pause-entries' ? ': entries resume' : ''}`;
|
|
125
|
+
}
|