dexterm-cli 0.1.7 → 0.2.0
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 +3 -1
- package/dist/packages/agents/src/tiers.js +9 -0
- package/dist/packages/agents/src/tools.js +10 -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 +2 -1
- 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 +3 -0
- package/dist/packages/autotrade/src/forward.js +355 -0
- package/dist/packages/autotrade/src/index.js +19 -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/runner.js +62 -11
- package/dist/packages/autotrade/src/service-guard.js +125 -0
- package/dist/packages/autotrade/src/service-install.js +213 -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/signal-channels.js +312 -0
- package/dist/packages/autotrade/src/signal.js +196 -0
- package/dist/packages/autotrade/src/strategy.js +3 -1
- package/dist/packages/autotrade/src/trade-ledger.js +2 -2
- 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 +14 -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 +8 -4
- 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 +257 -13
- 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/ledger-cmd.js +4 -2
- package/dist/packages/cli/src/pnl-card.js +4 -0
- 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/telegram-cmd.js +4 -1
- package/dist/packages/cli/src/version.js +1 -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/live-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -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-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 +57 -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-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 +39 -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 +24 -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 +1 -1
- 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-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-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 +35 -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-telegram-what-it-is.md +1 -1
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
- 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 +21 -10
- package/dist/packages/tui/src/keymap.js +7 -0
- package/package.json +1 -1
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
// Nothing here arms a mandate, signs, holds a key or touches a wallet. task-1571: there is no `--live`: dexterm places
|
|
11
11
|
// no live order. Live trading happens in the Telegram trading bot of the user's choice.
|
|
12
12
|
//
|
|
13
|
+
// task-1645: `--real` runs a strategy armed for real mode (`dexterm autotrade signal arm`, on the user's terminal) on
|
|
14
|
+
// LIVE data only: the same engine as paper, and every fill first goes out as a LIVE TRADE FIRE signal (outbox +
|
|
15
|
+
// the channels of connectors.json `signals`, the Telegram copy through the user's bot). Never on a replay (no stale
|
|
16
|
+
// price may ever reach an executor), never with chat approvals (a tap never authorises a real signal).
|
|
17
|
+
//
|
|
13
18
|
// Telegram (task-1527): when `dexterm telegram setup` stored a token and a chat is paired, the user's own bot starts
|
|
14
19
|
// with the run (telegram-host.ts): notify, control through the same control file, and approve-to-trade on paper (a
|
|
15
20
|
// strategy with `entry.decide: "agent+approve"`, or approvals on). --no-telegram keeps it off; an agent+approve
|
|
@@ -26,10 +31,14 @@ import { pickBasis } from '../../../data/src/basis.js';
|
|
|
26
31
|
import { KillSwitch, createDataLayer, defaultDbPath, resolveRpcUrl } from '../../../data/src/index.js';
|
|
27
32
|
import { HEARTBEAT_MS, LiveSource, badgeOf, RUNNER_RENDEZVOUS_SCHEMA, RunnerSafety, SessionGate, acquireRunLock, clearControl, clearStaleControl, controlPath, claudeSessionFactory, effectiveCaps, fileLedger, loadRunnerConfig, loadTape, newRunId, prepareAccount, releaseRunLock, renderDossierRecord, runAutotrade, runnerRendezvousPath, runnerSystemPrompt, simExecutor, stubSession, writeJsonAtomic, writeRendezvous, RUN_SUMMARY_SCHEMA, builtinTapePath, } from '../../../autotrade/src/index.js';
|
|
28
33
|
import { assertAutotradeAllowed, ollamaSessionFactory } from '../../../autotrade/src/ollama-sessions.js';
|
|
34
|
+
import { Outbox, RealSignalExecutor, channelsFromConfig, enabledChannels, loadSignalsConfig, openRealRun, realModeCheck } from '../../../autotrade/src/index.js';
|
|
35
|
+
import { PaperLimitsExecutor, effectiveLimits, loadLimits } from '../../../autotrade/src/index.js';
|
|
36
|
+
import { digestDue, digestLines, hmacStore, limitsSummary, loadDigestState, saveDigestState, startExecutorListener } from '../../../autotrade/src/index.js';
|
|
37
|
+
import { digestFor } from "./report.js";
|
|
29
38
|
import { parseArgs, resolveStrategyArg } from "./strategies.js";
|
|
30
39
|
import { startTelegramHost } from "./telegram-host.js";
|
|
31
|
-
const BOOL = ['paper', 'live', 'stub', 'verbose', 'quiet', 'raw', 'no-telegram'];
|
|
32
|
-
export const RUN_USAGE = 'dexterm autotrade run <id|file> --paper [--replay TAPE [--ms-per-tick 2000]] [--engine claude|ollama] [--model M] [--max-minutes N] [--stub] [--raw] [--quiet] [--no-telegram]';
|
|
40
|
+
const BOOL = ['paper', 'real', 'live', 'stub', 'verbose', 'quiet', 'raw', 'no-telegram'];
|
|
41
|
+
export const RUN_USAGE = 'dexterm autotrade run <id|file> --paper|--real [--replay TAPE [--ms-per-tick 2000]] [--engine claude|ollama] [--model M] [--max-minutes N] [--sol-usd N] [--stub] [--raw] [--quiet] [--no-telegram]';
|
|
33
42
|
/** task-1571: what `--live` says. dexterm holds no key and places no live order. */
|
|
34
43
|
export const NO_LIVE = 'dexterm autotrade run: there is no --live: dexterm runs strategies on paper only and never holds a key or signs; live trading happens in the Telegram trading bot of your choice (your own bot gets the alerts: dexterm telegram setup)';
|
|
35
44
|
function num(v, dflt) {
|
|
@@ -45,8 +54,12 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
45
54
|
io.stderr.write(`${NO_LIVE}\n`);
|
|
46
55
|
return 2;
|
|
47
56
|
}
|
|
48
|
-
if (!opts.paper) {
|
|
49
|
-
io.stderr.write(`dexterm autotrade run: pass --paper (
|
|
57
|
+
if (!opts.paper && !opts.real) {
|
|
58
|
+
io.stderr.write(`dexterm autotrade run: pass --paper (or --real for a strategy armed for real mode)\nusage: ${RUN_USAGE}\n`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
if (opts.paper && opts.real) {
|
|
62
|
+
io.stderr.write('dexterm autotrade run: --paper or --real, not both\n');
|
|
50
63
|
return 2;
|
|
51
64
|
}
|
|
52
65
|
const arg = typeof opts.strategy === 'string' ? opts.strategy : pos[0];
|
|
@@ -62,11 +75,17 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
62
75
|
io.stderr.write(`error ${e.path} ${e.msg}\n`);
|
|
63
76
|
return 1;
|
|
64
77
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
// task-1645: --paper on a real strategy runs its paper twin (a forward test: no signal goes out).
|
|
79
|
+
const strategy = opts.paper && r.strategy.mode === 'real' ? { ...r.strategy, mode: 'paper' } : r.strategy;
|
|
80
|
+
if (opts.real && strategy.mode !== 'real') {
|
|
81
|
+
io.stderr.write(`dexterm autotrade run: ${strategy.id} is mode "${strategy.mode}": --real needs a strategy armed for real mode (dexterm autotrade signal arm ${strategy.id}, on your own terminal)\n`);
|
|
68
82
|
return 1;
|
|
69
83
|
}
|
|
84
|
+
if (strategy.mode !== 'paper' && strategy.mode !== 'real') {
|
|
85
|
+
io.stderr.write(`dexterm autotrade run: ${strategy.id} is mode "${strategy.mode}": the runner runs paper only (dexterm holds no key and places no live order); real mode, which sends signals to your own executor, is armed with dexterm autotrade signal arm\n`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const realMode = strategy.mode === 'real';
|
|
70
89
|
const home = dextermHome(io.env);
|
|
71
90
|
// task-1537: the run's lines and the agents' messages speak the user's basis (--price/--mcap/env, config, market cap)
|
|
72
91
|
const basis = pickBasis(loadConfig(home).config.display?.priceMode, io.env);
|
|
@@ -83,6 +102,19 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
83
102
|
}
|
|
84
103
|
const config = cfg.config;
|
|
85
104
|
const replay = typeof opts.replay === 'string' ? opts.replay : null;
|
|
105
|
+
// task-1645: real mode's preconditions, before anything starts (armed at this hash, caps, kill off, a channel).
|
|
106
|
+
let real = null;
|
|
107
|
+
if (realMode) {
|
|
108
|
+
if (replay) {
|
|
109
|
+
io.stderr.write('dexterm autotrade run: --real runs on live data only (a replay would send old prices to your executor); replay its paper twin with --paper\n');
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
real = await prepareReal(home, strategy, opts, io);
|
|
113
|
+
if ('error' in real) {
|
|
114
|
+
io.stderr.write(`dexterm autotrade run: ${real.error}\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
86
118
|
// task-1543: the engine for the sessions: --engine, else ai.json (surfaces.autotrade / provider); auto = claude.
|
|
87
119
|
let local = null;
|
|
88
120
|
if (!opts.stub) {
|
|
@@ -100,6 +132,8 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
100
132
|
const lock = acquireRunLock(home, runId);
|
|
101
133
|
if (!lock.ok) {
|
|
102
134
|
io.stderr.write(`dexterm autotrade run: another runner is active (pid ${lock.holder.pid}, run ${lock.holder.runId}); stop it first\n`);
|
|
135
|
+
if (real && !('error' in real))
|
|
136
|
+
real.stop();
|
|
103
137
|
return 1;
|
|
104
138
|
}
|
|
105
139
|
if (lock.reclaimed)
|
|
@@ -109,7 +143,7 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
109
143
|
releaseRunLock(home, runId);
|
|
110
144
|
return 1;
|
|
111
145
|
}
|
|
112
|
-
const badge = 'PAPER';
|
|
146
|
+
const badge = realMode ? 'REAL (LIVE TRADE FIRE signals to your executor)' : 'PAPER';
|
|
113
147
|
// Telegram: the user's own bot, when it is set up and paired (or the host a /use switch carried over).
|
|
114
148
|
let host = over.telegramHost ?? null;
|
|
115
149
|
const ownHost = !over.telegramHost;
|
|
@@ -127,14 +161,103 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
127
161
|
else if (!t.quiet)
|
|
128
162
|
say(`telegram: off (${t.why})`);
|
|
129
163
|
}
|
|
164
|
+
if (realMode && host && (strategy.entry.decide === 'agent+approve' || host.service.broker.required(strategy))) {
|
|
165
|
+
io.stderr.write(`dexterm autotrade run: approve-to-trade over chat is paper only, so ${strategy.id} cannot take an entry in real mode (entry.decide "agent+approve" or \`dexterm telegram approvals on\`); your executor is the last gate in real mode\n`);
|
|
166
|
+
if (ownHost)
|
|
167
|
+
await host.stop();
|
|
168
|
+
releaseRunLock(home, runId);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
130
171
|
if (strategy.entry.decide === 'agent+approve' && !host) {
|
|
131
172
|
io.stderr.write(`dexterm autotrade run: ${strategy.id} needs a one-tap approval for every entry (entry.decide "agent+approve"), and no paired Telegram chat is connected; set it up with \`dexterm telegram setup\`\n`);
|
|
132
173
|
releaseRunLock(home, runId);
|
|
133
174
|
return 1;
|
|
134
175
|
}
|
|
176
|
+
// task-1654: a paper run obeys the limits only when limits.json has a `paper` scope (a forward test that shows what
|
|
177
|
+
// real mode would have refused or clamped). Its state is the run's own; real mode's files are never touched.
|
|
178
|
+
let paperLimits = null;
|
|
179
|
+
if (!realMode) {
|
|
180
|
+
paperLimits = await preparePaperLimits(home, strategy, opts, io, !!replay);
|
|
181
|
+
if (paperLimits && 'error' in paperLimits) {
|
|
182
|
+
io.stderr.write(`dexterm autotrade run: ${paperLimits.error}\n`);
|
|
183
|
+
if (host && ownHost)
|
|
184
|
+
await host.stop();
|
|
185
|
+
releaseRunLock(home, runId);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
135
189
|
const baseExec = simExecutor(strategy.execution);
|
|
136
190
|
if (host)
|
|
137
191
|
host.attach(runId, strategy, baseExec, solUsdFor);
|
|
192
|
+
// task-1645: the signal executor wraps the paper path (the host's port when the bot is on), so every fill is first a signal.
|
|
193
|
+
let realExec = null;
|
|
194
|
+
let outbox = null;
|
|
195
|
+
let pumpTimer = null;
|
|
196
|
+
let digestTimer = null;
|
|
197
|
+
let listener = null;
|
|
198
|
+
let bookUsd = strategy.bankroll.startUsd;
|
|
199
|
+
let paperExec = null;
|
|
200
|
+
if (paperLimits && !('error' in paperLimits)) {
|
|
201
|
+
paperExec = new PaperLimitsExecutor({ inner: host ? host.port : baseExec, limits: paperLimits.limits, solUsd: paperLimits.solUsd, bookUsd: () => bookUsd, onTrip: say });
|
|
202
|
+
}
|
|
203
|
+
if (real && !('error' in real)) {
|
|
204
|
+
const sc = real.signals;
|
|
205
|
+
const tg = host ? async (lines) => {
|
|
206
|
+
const before = host.service.audit.events.length;
|
|
207
|
+
await host.service.hub.broadcast({ kind: 'text', badge: 'LIVE', lines }, 'trade_fire');
|
|
208
|
+
if (host.service.audit.events.slice(before).some((e) => e.kind === 'out_failed'))
|
|
209
|
+
throw new Error('telegram send failed');
|
|
210
|
+
} : undefined;
|
|
211
|
+
const { channels, defer } = channelsFromConfig(home, sc, { telegram: tg, env: io.env });
|
|
212
|
+
try {
|
|
213
|
+
outbox = Outbox.open(home, { channels, defer });
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
io.stderr.write(`dexterm autotrade run: ${e.message}\n`);
|
|
217
|
+
if (host && ownHost)
|
|
218
|
+
await host.stop();
|
|
219
|
+
releaseRunLock(home, runId);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
const swept = openRealRun(outbox, runId, strategy, real.check.arming.accountId).swept;
|
|
223
|
+
if (swept.length)
|
|
224
|
+
say(`real mode: sent "halted" (runner-lost) for ${swept.length} earlier run(s) that ended without one`);
|
|
225
|
+
// task-1654: every limit (the effective set for this strategy + account), and a line to the run log and the
|
|
226
|
+
// paired chat when the executor goes silent / comes back or a risk limit trips.
|
|
227
|
+
const notify = (line) => { say(line); if (host)
|
|
228
|
+
void host.service.hub.broadcast({ kind: 'text', badge: 'LIVE', lines: [line] }, 'real_limits').catch(() => { }); };
|
|
229
|
+
realExec = new RealSignalExecutor({ home, inner: host ? host.port : baseExec, outbox, strategy, arming: real.check.arming, caps: real.check.caps, limits: real.check.limits.values, notify, runId, ttlSec: sc.ttlSec, solUsd: real.solUsd, bookUsd: () => bookUsd });
|
|
230
|
+
pumpTimer = setInterval(() => { void outbox.pump().catch(() => { }); }, 1000);
|
|
231
|
+
if (sc.executor.listenPort) {
|
|
232
|
+
const store = hmacStore(home, sc, io.env);
|
|
233
|
+
try {
|
|
234
|
+
listener = await startExecutorListener({ home, port: sc.executor.listenPort, secret: () => { try {
|
|
235
|
+
return store.get();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
} } });
|
|
240
|
+
say(`real mode: executor listener on 127.0.0.1:${listener.port} (POST /heartbeat, /receipt)`);
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
say(`real mode: executor listener NOT started (${e.message.slice(0, 80)}); executors can still append to autotrade/real/executor.jsonl`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// task-1654: the scheduled digest (opt-in: dexterm autotrade report --schedule daily|weekly), sent by a run with the bot on.
|
|
248
|
+
if (host) {
|
|
249
|
+
const tick = () => {
|
|
250
|
+
const st = loadDigestState(home);
|
|
251
|
+
const due = digestDue(st, Date.now());
|
|
252
|
+
if (!due)
|
|
253
|
+
return;
|
|
254
|
+
saveDigestState(home, { ...st, lastSentDay: localDayNow(), lastSentAt: Date.now() });
|
|
255
|
+
const d = digestFor(home, { period: due, now: Date.now() });
|
|
256
|
+
void host.service.hub.broadcast({ kind: 'text', badge: realMode ? 'LIVE' : 'PAPER', lines: digestLines(d) }, 'report').catch(() => { });
|
|
257
|
+
};
|
|
258
|
+
digestTimer = setInterval(tick, 10 * 60_000);
|
|
259
|
+
digestTimer.unref?.();
|
|
260
|
+
}
|
|
138
261
|
const { ledger, files } = fileLedger(home, runId, host?.onLedger);
|
|
139
262
|
const startedAt = Date.now();
|
|
140
263
|
const gate = new SessionGate(effectiveCaps(strategy, config), config.allowanceGuard);
|
|
@@ -226,7 +349,7 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
226
349
|
lastWrite = now;
|
|
227
350
|
const { t: _t, ...body } = last;
|
|
228
351
|
try {
|
|
229
|
-
writeRendezvous(home, { schema: RUNNER_RENDEZVOUS_SCHEMA, pid: process.pid, mode: 'paper', runId, startedAt, heartbeatAt: now, ledgerPath: files.ledger, ...body });
|
|
352
|
+
writeRendezvous(home, { schema: RUNNER_RENDEZVOUS_SCHEMA, pid: process.pid, mode: realMode ? 'real' : 'paper', runId, startedAt, heartbeatAt: now, ledgerPath: files.ledger, ...body });
|
|
230
353
|
writeJsonAtomic(files.summary, { schema: RUN_SUMMARY_SCHEMA, runId, strategy: body.strategy, startedAt, updatedAt: now, state: body.state, book: body.book, spend: body.spend, events: body.events, ledgerHead: body.ledgerHead, source: replay ? `replay ${replay}` : 'live' });
|
|
231
354
|
lastBeatOk = now;
|
|
232
355
|
if (beatFailing) {
|
|
@@ -256,14 +379,23 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
256
379
|
say(`⚠ HIGH RISK strategy (${strategy.id}): one position at a time, early entries, most of them lose. Paper only; not financial advice.`);
|
|
257
380
|
say(`dexterm autotrade run · ${badge} · strategy ${strategy.id} v${strategy.version} (${strategy.hash}) · ${replay ? `replay ${replay}` : 'live keyless data'} · sessions ${opts.stub ? 'stub' : local ? `ollama (local ${local.model}, profile ${local.level}, $0)` : `claude (${config.account.kind}${typeof opts.model === 'string' ? `, ${opts.model}` : ''})`}`);
|
|
258
381
|
say(`run ${runId} · ledger ${files.ledger} · rendezvous ${runnerRendezvousPath(home)}`);
|
|
382
|
+
if (realExec && real && !('error' in real)) {
|
|
383
|
+
say(`REAL MODE · account ${real.check.arming.accountId} · limits ${limitsSummary(real.check.limits.values)} · channels ${enabledChannels(real.signals).join(', ')} · ttl ${Math.min(real.signals.ttlSec, real.check.limits.values.signalTtlSec ?? Infinity)} s · kill: dexterm autotrade signal kill · limits: dexterm autotrade limits show`);
|
|
384
|
+
const carried = realExec.inherited().length;
|
|
385
|
+
const elsewhere = realExec.heldElsewhere();
|
|
386
|
+
if (carried || elsewhere.length)
|
|
387
|
+
say(`REAL HOLDINGS · ${carried} carried from an earlier run of ${strategy.id} (its exits keep running)${elsewhere.length ? ` · ${elsewhere.length} held by other strategies (${elsewhere.map((h) => `${h.symbol} ${h.strategyId}`).join(', ')}): counted against the open cap, not traded here` : ''}${realExec.holdingsSource === 'rebuilt' ? ' · rebuilt from the outbox' : ''} · dexterm autotrade signal held`);
|
|
388
|
+
}
|
|
389
|
+
if (paperExec && paperLimits && !('error' in paperLimits))
|
|
390
|
+
say(`PAPER LIMITS · the paper scope of limits.json applies · ${limitsSummary(paperLimits.limits)} · dexterm autotrade limits show --mode paper`);
|
|
259
391
|
if (host)
|
|
260
|
-
say(`telegram: on (your own bot, ${badgeOf(host.port.network)}) · approvals ${strategy.entry.decide === 'agent+approve' ? 'required by the strategy' : 'when turned on'} · /status /pause /kill from your paired chat`);
|
|
392
|
+
say(`telegram: on (your own bot, ${realMode ? 'LIVE copy of each signal' : badgeOf(host.port.network)}) · approvals ${strategy.entry.decide === 'agent+approve' ? 'required by the strategy' : 'when turned on'} · /status /pause /kill from your paired chat`);
|
|
261
393
|
let switchTo = null;
|
|
262
394
|
let code = 0;
|
|
263
395
|
try {
|
|
264
396
|
const res = await runAutotrade({
|
|
265
397
|
strategy, basis, tape, source, sessions, gate, dossier, vet, verbose: true, onLine: say, ledger, runId,
|
|
266
|
-
...(host ? { executor: host.port, approvals: host.service.broker } : { executor: baseExec }),
|
|
398
|
+
...(realExec ? { executor: realExec, real: realExec, ...(host ? { approvals: host.service.broker } : {}) } : paperExec ? { executor: paperExec, maxHoldMinutes: () => paperExec.maxHoldMinutes(), ...(host ? { approvals: host.service.broker } : {}) } : host ? { executor: host.port, approvals: host.service.broker } : { executor: baseExec }),
|
|
267
399
|
pacing: replay ? { mode: 'paced', msPerTick: num(opts['ms-per-tick'], 2000) } : { mode: 'live' },
|
|
268
400
|
// The panic file (`dexterm wallet panic`), a stop / flatten request and a local stop (signal, --max-minutes) all
|
|
269
401
|
// halt: no new entries, sessions close. A signal or --max-minutes also ends the tick stream at once.
|
|
@@ -273,7 +405,7 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
273
405
|
const v = safety.step();
|
|
274
406
|
return stopping && !v.halt ? { ...v, halt: 'runner-stopped', detail: 'a signal or --max-minutes' } : v;
|
|
275
407
|
},
|
|
276
|
-
onState: (st) => { runNow = st.t; last = st; writeState(st.state === 'stopped'); host?.onState(st); },
|
|
408
|
+
onState: (st) => { runNow = st.t; last = st; bookUsd = st.book.equityUsd; writeState(st.state === 'stopped'); host?.onState(st); },
|
|
277
409
|
onFocus: (marks) => { live?.setFocus(marks); writeFocus(home, marks); },
|
|
278
410
|
});
|
|
279
411
|
const next = host?.takeSwitch() ?? null;
|
|
@@ -287,6 +419,35 @@ export async function runCmd(argv, io, over = {}) {
|
|
|
287
419
|
}
|
|
288
420
|
finally {
|
|
289
421
|
clearInterval(beat);
|
|
422
|
+
if (pumpTimer)
|
|
423
|
+
clearInterval(pumpTimer);
|
|
424
|
+
if (digestTimer)
|
|
425
|
+
clearInterval(digestTimer);
|
|
426
|
+
if (listener)
|
|
427
|
+
await listener.close().catch(() => { });
|
|
428
|
+
// The runner always says goodbye: idempotent after a normal end, and the only `halted` when the run threw.
|
|
429
|
+
if (realExec) {
|
|
430
|
+
realExec.sendHalted(code ? 'runner-error' : 'runner-stopped');
|
|
431
|
+
// task-1679 (0.2.0 decision): what is still open when a real run ends (stop, kill, time limit, error) is impossible to miss.
|
|
432
|
+
const open = realExec.openOnAccount();
|
|
433
|
+
if (open.length) {
|
|
434
|
+
const line = `REAL POSITIONS STILL OPEN (${open.length}): ${open.map((h) => `${h.symbol} ${h.mint}${h.strategyId === strategy.id ? '' : ` (${h.strategyId})`}`).join(', ')}`;
|
|
435
|
+
say(line);
|
|
436
|
+
say(' these positions are still open: manage them in your executor. While no real run of the strategy that holds one is running, dexterm sends NO signal for it, sells included. The next real run of that strategy on this account carries it and its exits fire again (the "halted" signal lists them; dexterm autotrade signal held).');
|
|
437
|
+
if (host)
|
|
438
|
+
void host.service.hub.broadcast({ kind: 'text', badge: 'LIVE', lines: [line, 'these positions are still open: manage them in your executor'] }, 'real_limits').catch(() => { });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (outbox) {
|
|
442
|
+
try {
|
|
443
|
+
await outbox.pump();
|
|
444
|
+
}
|
|
445
|
+
catch { /* pending signals stay in the outbox; they expire by TTL */ }
|
|
446
|
+
outbox.close();
|
|
447
|
+
}
|
|
448
|
+
real?.stop?.();
|
|
449
|
+
if (paperLimits && !('error' in paperLimits))
|
|
450
|
+
paperLimits.stop();
|
|
290
451
|
process.removeListener('SIGINT', onSig);
|
|
291
452
|
process.removeListener('SIGTERM', onSig);
|
|
292
453
|
anchorChat('end');
|
|
@@ -335,7 +496,7 @@ function writeFocus(home, marks) {
|
|
|
335
496
|
* task-1543: resolve the session engine. claude = the original path (null here). ollama = the user's local model,
|
|
336
497
|
* allowed only when the local profile allows autotrade (paper) — checked before the run lock.
|
|
337
498
|
*/
|
|
338
|
-
async function localEngine(opts, io) {
|
|
499
|
+
export async function localEngine(opts, io) {
|
|
339
500
|
const eng = await import('../../../mcp/src/engine/index.js');
|
|
340
501
|
const loaded = eng.loadAiConfig(io.env);
|
|
341
502
|
const flag = typeof opts.engine === 'string' ? eng.parseEngineChoice(opts.engine) : null;
|
|
@@ -411,3 +572,86 @@ export function larpVet(g, check, timeoutMs = 20_000) {
|
|
|
411
572
|
return { ok: true, note: `LARP score ${score}${band ? ` (${band})` : ''} ≤ ${g.maxScore}` };
|
|
412
573
|
};
|
|
413
574
|
}
|
|
575
|
+
const SOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
576
|
+
/**
|
|
577
|
+
* task-1645: real mode's preconditions (realModeCheck: armed at this hash, kill off, caps, an executor channel) and
|
|
578
|
+
* SOL/USD for sizes in SOL: `--sol-usd N`, else the SOL pair on DexScreener (keyless), refreshed every minute.
|
|
579
|
+
* No SOL price, no run: a signal must state its size in SOL.
|
|
580
|
+
*/
|
|
581
|
+
async function prepareReal(home, s, opts, io) {
|
|
582
|
+
const sc = loadSignalsConfig(home);
|
|
583
|
+
if (sc.problems.length)
|
|
584
|
+
io.stderr.write(`warning: signal channels turned off: ${sc.problems.join('; ')}\n`);
|
|
585
|
+
const check = realModeCheck(home, s, enabledChannels(sc.config));
|
|
586
|
+
if (!check.ok)
|
|
587
|
+
return { error: check.why };
|
|
588
|
+
const fixed = typeof opts['sol-usd'] === 'string' ? Number(opts['sol-usd']) : null;
|
|
589
|
+
if (fixed !== null && !(fixed > 1 && fixed < 100_000))
|
|
590
|
+
return { error: '--sol-usd needs the SOL price in USD' };
|
|
591
|
+
if (fixed)
|
|
592
|
+
return { check, signals: sc.config, solUsd: () => fixed, stop: () => { } };
|
|
593
|
+
const { createDataLayer, defaultDbPath } = await import('../../../data/src/index.js');
|
|
594
|
+
const data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env) });
|
|
595
|
+
let px = null;
|
|
596
|
+
const refresh = async () => { try {
|
|
597
|
+
const p = (await data.dexscreener.tokens([SOL_MINT])).get(SOL_MINT)?.pair.priceUsd ?? null;
|
|
598
|
+
if (p && p > 1 && p < 100_000)
|
|
599
|
+
px = p;
|
|
600
|
+
}
|
|
601
|
+
catch { /* keep the last price */ } };
|
|
602
|
+
await refresh();
|
|
603
|
+
if (!px) {
|
|
604
|
+
data.close();
|
|
605
|
+
return { error: 'no SOL/USD price (DexScreener did not answer); pass --sol-usd N' };
|
|
606
|
+
}
|
|
607
|
+
const t = setInterval(() => { void refresh(); }, 60_000);
|
|
608
|
+
t.unref?.();
|
|
609
|
+
return { check, signals: sc.config, solUsd: () => px, stop: () => { clearInterval(t); data.close(); } };
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* task-1654: the `paper` scope of limits.json for a paper run, or null when there is none (paper ignores the limits
|
|
613
|
+
* then, exactly as before). Sizes are in SOL, so it needs SOL/USD: `--sol-usd N`, else DexScreener on live data; a
|
|
614
|
+
* replay needs --sol-usd (today's price is not the tape's). An unreadable or invalid file is only a warning: a
|
|
615
|
+
* paper run moves no money, and the line says the paper scope was NOT applied.
|
|
616
|
+
*/
|
|
617
|
+
async function preparePaperLimits(home, s, opts, io, replay) {
|
|
618
|
+
const l = loadLimits(home);
|
|
619
|
+
if (!l.ok) {
|
|
620
|
+
if (!('missing' in l && l.missing))
|
|
621
|
+
io.stderr.write(`warning: paper limits NOT applied: ${l.why}\n`);
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
const accountId = typeof opts.account === 'string' ? opts.account : '-';
|
|
625
|
+
const e = effectiveLimits(l.doc, { strategyId: s.id, accountId, mode: 'paper' });
|
|
626
|
+
if (!e.applies)
|
|
627
|
+
return null;
|
|
628
|
+
const fixed = typeof opts['sol-usd'] === 'string' ? Number(opts['sol-usd']) : null;
|
|
629
|
+
if (fixed !== null && !(fixed > 1 && fixed < 100_000))
|
|
630
|
+
return { error: '--sol-usd needs the SOL price in USD' };
|
|
631
|
+
if (fixed)
|
|
632
|
+
return { limits: e.values, solUsd: () => fixed, stop: () => { } };
|
|
633
|
+
if (replay)
|
|
634
|
+
return { error: 'limits.json has a paper scope (sizes in SOL): a replay needs the SOL price of its day, --sol-usd N' };
|
|
635
|
+
const { createDataLayer, defaultDbPath } = await import('../../../data/src/index.js');
|
|
636
|
+
const data = createDataLayer({ dbPath: io.env.DEXTERM_DB_PATH || defaultDbPath(io.env) });
|
|
637
|
+
let px = null;
|
|
638
|
+
const refresh = async () => { try {
|
|
639
|
+
const p = (await data.dexscreener.tokens([SOL_MINT])).get(SOL_MINT)?.pair.priceUsd ?? null;
|
|
640
|
+
if (p && p > 1 && p < 100_000)
|
|
641
|
+
px = p;
|
|
642
|
+
}
|
|
643
|
+
catch { /* keep the last price */ } };
|
|
644
|
+
await refresh();
|
|
645
|
+
if (!px) {
|
|
646
|
+
data.close();
|
|
647
|
+
return { error: 'limits.json has a paper scope (sizes in SOL) and there is no SOL/USD price (DexScreener did not answer); pass --sol-usd N' };
|
|
648
|
+
}
|
|
649
|
+
const t = setInterval(() => { void refresh(); }, 60_000);
|
|
650
|
+
t.unref?.();
|
|
651
|
+
return { limits: e.values, solUsd: () => px, stop: () => { clearInterval(t); data.close(); } };
|
|
652
|
+
}
|
|
653
|
+
/** The local calendar day (the digest schedule's key). */
|
|
654
|
+
function localDayNow(t = Date.now()) {
|
|
655
|
+
const d = new Date(t);
|
|
656
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
657
|
+
}
|