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.
Files changed (119) hide show
  1. package/dist/packages/agents/src/spectator.js +3 -1
  2. package/dist/packages/agents/src/tiers.js +9 -0
  3. package/dist/packages/agents/src/tools.js +10 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +2 -1
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +3 -0
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +19 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/runner.js +62 -11
  30. package/dist/packages/autotrade/src/service-guard.js +125 -0
  31. package/dist/packages/autotrade/src/service-install.js +213 -0
  32. package/dist/packages/autotrade/src/service-recover.js +232 -0
  33. package/dist/packages/autotrade/src/service.js +289 -0
  34. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  35. package/dist/packages/autotrade/src/signal.js +196 -0
  36. package/dist/packages/autotrade/src/strategy.js +3 -1
  37. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  38. package/dist/packages/cli/src/analyst/backend.js +3 -2
  39. package/dist/packages/cli/src/analyst/codex.js +3 -2
  40. package/dist/packages/cli/src/app-help-gen.js +14 -2
  41. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  42. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  43. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  44. package/dist/packages/cli/src/autotrade-cmd/fleet.js +8 -4
  45. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  46. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  47. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  48. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  49. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  50. package/dist/packages/cli/src/autotrade-cmd/run.js +257 -13
  51. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  52. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  53. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  54. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  55. package/dist/packages/cli/src/pnl-card.js +4 -0
  56. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  57. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  58. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  59. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  60. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  61. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  62. package/dist/packages/cli/src/version.js +1 -1
  63. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  64. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  65. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  66. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  67. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  68. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  69. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  70. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  71. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  72. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  73. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  74. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  75. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
  76. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  77. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  78. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  79. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  80. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  81. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  83. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
  84. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  85. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  86. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
  87. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  88. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  89. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  90. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  91. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  92. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  93. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  94. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  95. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  96. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  97. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  98. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  99. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  100. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  101. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  102. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  105. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  107. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  109. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  110. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  111. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  112. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  113. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  114. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  115. package/dist/packages/mcp/src/index.js +1 -1
  116. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  117. package/dist/packages/mcp/src/server.js +21 -10
  118. package/dist/packages/tui/src/keymap.js +7 -0
  119. package/package.json +1 -1
@@ -11,9 +11,17 @@
11
11
  // every candidate of a run (what the skips turned into)
12
12
  // telegram_status observe the Telegram connector's settings: enabled, paired chats (a count), approvals, notify
13
13
  // trading_status observe where live trading happens (task-1571: not in dexterm; it holds no key and places no
14
- // live order) and whether the panic switch is engaged
14
+ // live order), whether the panic switch is engaged, and real mode's state (task-1645)
15
+ // trade_fire_feed observe task-1645: the MCP channel of real mode: the LIVE TRADE FIRE signals of the feed
16
+ // (autotrade/real/mcp-feed.jsonl) after a nonce, and real mode's status. Reading is all it
17
+ // does: no tool arms real mode, changes a cap, lifts the kill switch or records a receipt
18
+ // trading_limits observe task-1654: real mode's limits in force (for a strategy / account / mode), where each
19
+ // number comes from, what each limit does, today's counters and the executor's health
15
20
  // backtest_requests observe the backtests this agent asked for, and their results once the user ran them
16
21
  // backtest_request propose ask the user to run a longer backtest (a request file; the user runs or dismisses it)
22
+ // limits_propose propose task-1654: a limits change as a DIFF CARD (old -> new, which numbers it raises, the
23
+ // exact command the user would type). It writes nothing at all: only the user applies a
24
+ // change, on their own terminal (`dexterm autotrade limits set`; a raise is typed)
17
25
  //
18
26
  // Nothing here can trade, sign, change a cap, enable live or touch a key: the ledger is read through
19
27
  // `@dexterm/autotrade`'s loader (pure file reads, loaded lazily: @dexterm/autotrade imports this package). Text that
@@ -26,10 +34,10 @@ import { dextermHome } from "./user-content.js";
26
34
  import { BacktestRequests, MAX_PENDING_REQUESTS, REQUEST_MAX_DAYS, REQUEST_RATE_PER_HOUR } from "./backtest-requests.js";
27
35
  export const LEDGER_READ_TOOLS = [
28
36
  'ledger_trades', 'ledger_positions', 'ledger_equity', 'ledger_summary', 'ledger_compare',
29
- 'candidate_dossier', 'outcomes_lookup', 'telegram_status', 'trading_status', 'backtest_requests',
37
+ 'candidate_dossier', 'outcomes_lookup', 'telegram_status', 'trading_status', 'trade_fire_feed', 'trading_limits', 'backtest_requests',
30
38
  ];
31
39
  /** Write-shaped (a request file), so not on the read-only surface a skill may name. */
32
- export const LEDGER_PROPOSE_TOOLS = ['backtest_request'];
40
+ export const LEDGER_PROPOSE_TOOLS = ['backtest_request', 'limits_propose'];
33
41
  export const LEDGER_TOOLS = [...LEDGER_READ_TOOLS, ...LEDGER_PROPOSE_TOOLS];
34
42
  const MINT = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
35
43
  const RUN_ID = /^[0-9TZ]+-[a-z][a-z0-9-]{1,31}-[0-9a-f]{4}$/;
@@ -64,10 +72,10 @@ export function filterFromArgs(a, now) {
64
72
  return allowed.includes(a[k]) ? a[k] : false;
65
73
  };
66
74
  const source = pick('source', ['paper', 'desk', 'autotrade', 'live']);
67
- const mode = pick('mode', ['paper', 'devnet', 'live']);
75
+ const mode = pick('mode', ['paper', 'devnet', 'live', 'real']);
68
76
  const status = pick('status', ['open', 'closed']);
69
77
  if (source === false || mode === false || status === false)
70
- return { ok: false, error: 'source is paper|desk|autotrade|live, mode is paper|devnet|live, status is open|closed' };
78
+ return { ok: false, error: 'source is paper|desk|autotrade|live, mode is paper|devnet|live|real, status is open|closed' };
71
79
  if (source)
72
80
  f.source = source;
73
81
  if (mode)
@@ -135,6 +143,9 @@ export class LedgerTools {
135
143
  case 'outcomes_lookup': return await this.outcomes(args);
136
144
  case 'telegram_status': return this.telegram();
137
145
  case 'trading_status': return this.trading();
146
+ case 'trade_fire_feed': return this.signals(args);
147
+ case 'trading_limits': return await this.limits(args);
148
+ case 'limits_propose': return await this.limitsPropose(args);
138
149
  case 'backtest_requests': return this.requests();
139
150
  case 'backtest_request': return await this.request(args);
140
151
  default: return { error: `unknown ledger tool ${name}` };
@@ -154,7 +165,7 @@ export class LedgerTools {
154
165
  const rows = filterTrades(v.trades, filter);
155
166
  const integrity = { ledger: v.file.check.ok ? `verified, ${v.file.events.length} events` : `FAILS its check at line ${v.file.check.at}: ${v.file.check.error}`, pendingFromSources: v.pending.length, problems: v.sources.filter((s) => s.problem).map((s) => s.problem) };
156
167
  const modes = [...new Set(rows.map((t) => t.walletMode))];
157
- const base = { readOnly: true, modes, integrity, note: modes.length > 1 ? 'PAPER, DEVNET and LIVE trades are in this set: filter by mode before comparing results.' : modes.includes('paper') ? 'PAPER (simulated) trades: never present them as results.' : undefined };
168
+ const base = { readOnly: true, modes, integrity, note: modes.length > 1 ? 'PAPER, REAL, DEVNET and LIVE trades are in this set: filter by mode before comparing results.' : modes.includes('paper') ? 'PAPER (simulated) trades: never present them as results.' : modes.includes('real') ? 'REAL-mode trades: dexterm\'s model of the trades it signalled to the user\'s executor; the executor\'s receipts are the real fills.' : undefined };
158
169
  if (name === 'ledger_trades' || name === 'ledger_positions') {
159
170
  const limit = Math.min(200, Math.max(1, Number.isInteger(args.limit) ? Number(args.limit) : 50));
160
171
  const newest = [...rows].sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
@@ -292,13 +303,150 @@ export class LedgerTools {
292
303
  // ───────────────────────────── live trading (task-1571: not in dexterm) ───────────────
293
304
  trading() {
294
305
  const panic = existsSync(join(this.home(), 'trading.panic'));
306
+ const real = this.realState();
295
307
  return {
296
308
  readOnly: true,
297
- liveTrading: 'not in dexterm: dexterm holds no key, signs nothing and places no live order. Live trading happens in the Telegram trading bot the user chose, which holds its own wallet; dexterm only sends alerts and run control to the user\'s own bot (telegram_status).',
309
+ liveTrading: 'not in dexterm: dexterm holds no key, signs nothing and places no live order. Live trading happens in the Telegram trading bot (or script) the user chose, which holds its own wallet; dexterm only sends alerts, run control and, in real mode, LIVE TRADE FIRE signals to the user\'s own executor (telegram_status, trade_fire_feed).',
298
310
  paperTrading: 'local: the paper ticket, paper accounts and paper autotrade runs (ledger_* tools)',
311
+ realMode: `${real.armed.length} strategy(ies) armed for real mode by the user on their own terminal${real.kill ? `; the real-mode kill switch is ON (${real.kill})` : ''}. Only the user arms, disarms, sets caps or lifts the kill switch, on their own terminal; no tool can.`,
299
312
  panicSwitch: panic ? 'engaged (autotrade runs and the paper agent desk are halted; only `dexterm wallet resume` in a terminal clears it)' : 'off',
300
313
  };
301
314
  }
315
+ // ───────────────────────────── real mode (task-1645) ─────────────────────────────
316
+ readJson(path) {
317
+ try {
318
+ return JSON.parse(readFileSync(path, 'utf8'));
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ /** Real mode's state from its files (pure reads; ids and numbers only). */
325
+ realState() {
326
+ const dir = join(this.home(), 'autotrade', 'real');
327
+ const arming = this.readJson(join(dir, 'arming.json'));
328
+ const armed = Object.values((arming?.armed ?? {}))
329
+ .filter((a) => typeof a.strategyId === 'string' && /^[a-z][a-z0-9-]{1,31}$/.test(a.strategyId))
330
+ .map((a) => ({ id: String(a.strategyId), version: String(a.version ?? '').slice(0, 16), accountId: String(a.accountId ?? '').replace(/[^a-z0-9_-]/g, '').slice(0, 32) }));
331
+ const killFile = join(dir, 'kill.json');
332
+ const k = existsSync(killFile) ? this.readJson(killFile) : null;
333
+ // task-1654: the caps are four of the limits (limits.json global, else card A's caps.json).
334
+ const lim = this.readJson(join(dir, 'limits.json'))?.global;
335
+ const caps = (lim ? Object.fromEntries(['perTradeSol', 'dailySol', 'buysPerDay', 'maxOpenPositions'].filter((k) => typeof lim[k] === 'number').map((k) => [k, lim[k]])) : undefined) ?? this.readJson(join(dir, 'caps.json'))?.caps;
336
+ const capsNum = caps ? Object.fromEntries(Object.entries(caps).filter(([, v]) => typeof v === 'number')) : null;
337
+ const n = Number(this.readJson(join(dir, 'nonce.json'))?.last);
338
+ return { armed, kill: existsSync(killFile) ? cleanLedgerText(k?.reason ?? 'engaged', 80) || 'engaged' : null, caps: capsNum, lastNonce: Number.isSafeInteger(n) ? n : null };
339
+ }
340
+ signals(args) {
341
+ const after = Number(args.after_nonce ?? 0);
342
+ const limit = Number(args.limit ?? 50);
343
+ if (!Number.isSafeInteger(after) || after < 0)
344
+ return { error: 'after_nonce is a whole number >= 0' };
345
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
346
+ return { error: 'limit is a whole number from 1 to 100' };
347
+ const feed = join(this.home(), 'autotrade', 'real', 'mcp-feed.jsonl');
348
+ const out = [];
349
+ let feedOn = false;
350
+ if (existsSync(feed)) {
351
+ feedOn = true;
352
+ for (const l of readFileSync(feed, 'utf8').split('\n')) {
353
+ if (!l.trim())
354
+ continue;
355
+ let x;
356
+ try {
357
+ x = JSON.parse(l);
358
+ }
359
+ catch {
360
+ continue;
361
+ }
362
+ if (x.schema !== 'dexterm-trade-fire/1' || typeof x.nonce !== 'number' || x.nonce <= after)
363
+ continue;
364
+ // The signal as dexterm wrote it; the only token text in it is the symbol, already reduced to [A-Za-z0-9._$-].
365
+ out.push({ ...x, symbol: typeof x.symbol === 'string' ? x.symbol.replace(/[^A-Za-z0-9._$-]/g, '').slice(0, 16) : undefined });
366
+ if (out.length >= limit)
367
+ break;
368
+ }
369
+ }
370
+ const st = this.realState();
371
+ return {
372
+ readOnly: true, schema: 'dexterm-trade-fire/1', signals: out, nextAfterNonce: out.length ? out[out.length - 1].nonce : after,
373
+ realMode: { armed: st.armed, kill: st.kill, caps: st.caps, lastNonce: st.lastNonce, mcpFeed: feedOn ? 'on' : 'off (the user enables it: dexterm autotrade signal channels --mcp on)' },
374
+ rules: 'An executor acts only on its own accountId, ignores a signal after expiresAt, and dedupes by nonce (delivery is at-least-once). type "halted" ends a run; type "test" is never traded. The user records what was done with `dexterm autotrade receipt`; this tool changes nothing.',
375
+ };
376
+ }
377
+ // ───────────────────────────── limits (task-1654) ─────────────────────────────
378
+ limitsContext(args) {
379
+ const id = (v, name) => (v === undefined ? '-' : typeof v === 'string' && /^[a-z0-9][a-z0-9._-]{0,63}$/.test(v) ? v : { error: `${name} is an id ([a-z0-9._-], 64 max)` });
380
+ const strategyId = id(args.strategy, 'strategy');
381
+ if (typeof strategyId !== 'string')
382
+ return strategyId;
383
+ const accountId = id(args.account, 'account');
384
+ if (typeof accountId !== 'string')
385
+ return accountId;
386
+ if (args.mode !== undefined && args.mode !== 'real' && args.mode !== 'paper')
387
+ return { error: 'mode is real or paper' };
388
+ return { strategyId, accountId, mode: args.mode === 'paper' ? 'paper' : 'real' };
389
+ }
390
+ async limits(args) {
391
+ const c = this.limitsContext(args);
392
+ if ('error' in c)
393
+ return c;
394
+ const at = await import('../../autotrade/src/index.js');
395
+ const home = this.home();
396
+ const l = at.loadLimits(home);
397
+ const doc = l.ok ? l.doc : null;
398
+ const e = at.effectiveLimits(doc, c);
399
+ const risk = at.loadRiskState(home, this.now());
400
+ const ex = at.executorHealth(home, { now: this.now(), windowMin: e.values.executorSilenceMin ?? null });
401
+ return {
402
+ readOnly: true, context: c, applies: e.applies,
403
+ state: l.ok ? (l.source === 'migrated' ? 'migrated from caps.json' : l.unjournaled ? 'changed outside dexterm (a real run refuses it if it raises anything)' : 'saved by the user') : l.missing ? 'not set yet: placeholder defaults (real mode does not arm on them silently)' : 'unreadable: real buys are refused',
404
+ limits: at.LIMIT_FIELDS.map((f) => ({ key: f.key, group: f.group, unit: f.unit, kind: f.dir === 'max' ? 'cap (smaller is stricter)' : 'floor (larger is stricter)', value: e.values[f.key] ?? null, from: e.from[f.key] ?? null, placeholder: at.DEFAULT_LIMITS[f.key] ?? null, what: f.help })),
405
+ today: { day: risk.day, buys: risk.buys, buySol: risk.buySol, realizedSol: risk.dayPnlSol, lossStreak: risk.lossStreak, pausedUntil: iso(risk.pausedUntil), breaker: risk.breaker ? { at: iso(risk.breaker.at), drawdownSol: risk.breaker.drawdownSol } : null },
406
+ executor: at.healthLine(ex),
407
+ rules: 'Limits live outside the model. The effective number is the placeholder default replaced by the user\'s global value, then the STRICTER of every scope that sets it (mode, strategy, account). No tool changes them: limits_propose returns a diff card for the user to apply on their own terminal (raising needs their typed confirmation). The defaults are placeholders, not advice.',
408
+ };
409
+ }
410
+ async limitsPropose(args) {
411
+ const at = await import('../../autotrade/src/index.js');
412
+ const scope = typeof args.scope === 'string' ? args.scope : 'global';
413
+ const changes = args.changes;
414
+ if (!changes || typeof changes !== 'object' || Array.isArray(changes) || !Object.keys(changes).length || Object.keys(changes).length > 24)
415
+ return { error: 'changes is an object of 1 to 24 limits: { "perTradeSol": 0.1, "dailyLossSol": null }' };
416
+ // The agent's own reason is returned apart from the card, firewalled: the card is engine-made lines only.
417
+ const why = typeof args.why === 'string' ? fw(args.why.slice(0, 200), 'limits-why') : null;
418
+ const home = this.home();
419
+ const l = at.loadLimits(home);
420
+ const cur = l.ok ? l.doc : at.emptyLimits();
421
+ const next = structuredClone(cur);
422
+ delete next.migratedFrom;
423
+ const ref = at.scopeRef(next, scope, true);
424
+ if ('error' in ref)
425
+ return { error: ref.error };
426
+ for (const [k, v] of Object.entries(changes)) {
427
+ if (!at.limitField(k))
428
+ return { error: `unknown limit "${String(k).slice(0, 40)}"` };
429
+ if (v !== null && (typeof v !== 'number' || !Number.isFinite(v)))
430
+ return { error: `${k}: a number, or null to remove it` };
431
+ if (v === null && !ref.isGlobal)
432
+ delete ref.set[k];
433
+ else
434
+ ref.set[k] = v;
435
+ }
436
+ const issues = at.validateLimits(next);
437
+ if (issues.length)
438
+ return { error: issues.map((i) => `${i.path}: ${i.msg}`).join('; ').slice(0, 400) };
439
+ const diff = at.diffLimits(l.ok ? cur : null, next);
440
+ const up = at.raises(diff);
441
+ const assign = Object.entries(changes).map(([k, v]) => `${k}=${v === null ? 'off' : v}`).join(' ');
442
+ return {
443
+ applied: false, proposal: true,
444
+ card: [`PROPOSED LIMITS CHANGE · ${scope}`, ...diff.map((d) => ` ${at.changeLine(d)}`), up.length ? ` raises ${up.length} limit(s): you would type the confirmation on your own terminal` : ' lowers only: applies at once when you run it', ` to apply it yourself: dexterm autotrade limits set --scope ${scope} ${assign}`],
445
+ changes: diff, raises: up.length, why,
446
+ command: `dexterm autotrade limits set --scope ${scope} ${assign}`,
447
+ note: 'Nothing was changed: no tool can change a limit. Only the user applies this, on their own terminal.',
448
+ };
449
+ }
302
450
  // ───────────────────────────── backtest requests ─────────────────────────────
303
451
  requests() {
304
452
  const rs = new BacktestRequests(this.home()).list().slice(0, 20);
@@ -24,7 +24,7 @@ import { firewall } from "./firewall.js";
24
24
  import { OverlayStore, dextermHome, KB_RULE } from "./user-content.js";
25
25
  import { resolveSpectatorSink, readFollowScreen, FOLLOW_NAV_ACTIONS } from "./spectator-sink.js";
26
26
  import { resolvePaperAgentDesk } from "./paper-agent.js";
27
- import { AutotradeTools, AUTOTRADE_TOOLS, PARALLEL_CONTROL_TOOLS, PARALLEL_TOOLS, STRATEGY_PROPOSE_TOOLS } from "./autotrade-tools.js";
27
+ import { AutotradeTools, AUTOTRADE_TOOLS, JOURNEY_CONTROL_TOOLS, JOURNEY_PROPOSE_TOOLS, JOURNEY_TOOLS, PARALLEL_CONTROL_TOOLS, PARALLEL_TOOLS, STRATEGY_PROPOSE_TOOLS } from "./autotrade-tools.js";
28
28
  import { LedgerTools, LEDGER_TOOLS, LEDGER_PROPOSE_TOOLS } from "./ledger-tools.js";
29
29
  import { loadAppHelp, getAppHelp, loadDocsKb, getDocs, searchHelp } from "./app-help.js";
30
30
  import { assembleTokenDossier, computeBoard, recentNarrativeRows, loadDefaultLexicon, summaryText, buildDigest, tokenRugScorer, h1Model, runBacktest } from '../../packs/src/index.js';
@@ -309,8 +309,16 @@ const TOOLS_RAW = [
309
309
  { name: 'strategy_validate', description: 'AUTOTRADE (paper): validate a whole dexterm-strategy/1 document: {ok, errors[{path,msg}], warnings[]}. Saves nothing.', inputSchema: { type: 'object', properties: { doc: { type: 'object' } }, required: ['doc'], additionalProperties: false } },
310
310
  { name: 'strategy_propose_edit', description: 'AUTOTRADE (paper): PROPOSE a change to one of the user\'s strategies as JSON-patch ops (add/replace/remove, at most 20) with a one-line why. Returns a proposal (diff lines, validation, riskDelta tighter/looser/neutral with the reasons). It is NEVER applied by this tool: the user applies it with a key on the Q screen, and a looser one needs a second key. /mode, /bankroll, /agent/budgetUsdPerDay and /execution/minExitLiquidityUsd are user-only and refused. Paper only.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, ops: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'object', properties: { op: { type: 'string', enum: ['add', 'replace', 'remove'] }, path: { type: 'string', maxLength: 200 }, value: {} }, required: ['op', 'path'], additionalProperties: false } }, why: { type: 'string', maxLength: 200 } }, required: ['id', 'ops', 'why'], additionalProperties: false } },
311
311
  { name: 'strategy_propose_new', description: 'AUTOTRADE (paper): PROPOSE a new strategy copied from a preset (scalp, swing, runner, moonbag or another strategy id) under a new id and name, with optional JSON-patch ops (at most 20). Always paper. Same proposal shape as strategy_propose_edit; never applied by this tool (the user confirms it with a key).', inputSchema: { type: 'object', properties: { fromPreset: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, name: { type: 'string', maxLength: 80 }, ops: { type: 'array', maxItems: 20, items: { type: 'object', properties: { op: { type: 'string', enum: ['add', 'replace', 'remove'] }, path: { type: 'string', maxLength: 200 }, value: {} }, required: ['op', 'path'], additionalProperties: false } }, why: { type: 'string', maxLength: 200 } }, required: ['fromPreset', 'id', 'name'], additionalProperties: false } },
312
- { name: 'strategy_backtest', description: 'AUTOTRADE (paper): backtest a strategy (`id`) or a pending proposal next to its current version (`proposalId`) on the RECORDED history, days <= 14 (default 7): P/L, win rate with its Wilson interval, max drawdown, exits, clamps and the data limits. Modelled fills, not a prediction; quote the trade count. Read-only (nothing is saved), one at a time and rate-limited.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, proposalId: { type: 'string', maxLength: 40 }, tape: { type: 'string', enum: ['recorded'] }, days: { type: 'integer', minimum: 1, maximum: 14 } }, additionalProperties: false } },
312
+ { name: 'strategy_backtest', description: 'AUTOTRADE (paper): backtest a strategy (`id`) or a pending proposal next to its current version (`proposalId`) on the RECORDED history, days <= 14 (default 7): P/L, win rate with its Wilson interval, max drawdown, exits, clamps and the data limits. With `id` it also runs the strategy on each half of the held-out split (the newest 30% of coins) and records that as the strategy\'s backtest evidence for its journey (`heldOut`: train, test, verdict; gain wording only when `claim` is true). Modelled fills, not a prediction; quote the trade count. Never changes a strategy; one at a time and rate-limited.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, proposalId: { type: 'string', maxLength: 40 }, tape: { type: 'string', enum: ['recorded'] }, days: { type: 'integer', minimum: 1, maximum: 14 } }, additionalProperties: false } },
313
313
  { name: 'autotrade_status', description: 'AUTOTRADE (paper): the paper runner\'s state from its status file (heartbeat, halt reason, candidates, sessions and spend, open positions), the number of strategies and the proposals waiting for the user. Read-only.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
314
+ // ---- THE JOURNEY (task-1647, 0.2.0 card C): draft -> backtested -> forward-tested -> always-on -> real. Paper forward
315
+ // tests with the strategy's own bankroll, and promotion PROPOSALS the user applies (always-on: y; real: their own caps
316
+ // and a typed phrase on their terminal). No tool applies a promotion, sets a cap, arms real mode or skips a rung.
317
+ { name: 'strategy_journey', description: 'AUTOTRADE: where one of the user\'s strategies stands on the ladder draft → backtested → forward-tested → always-on (paper service) → real: each rung, the evidence on these exact rules (a rule change starts the ladder again), what the next rung needs, any forward test running and the promotions waiting for the user. Read-only.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' } }, required: ['id'], additionalProperties: false } },
318
+ { name: 'strategy_forward_start', description: 'AUTOTRADE (paper): start a FORWARD TEST of one of the user\'s strategies: its current version on LIVE data, on paper, with the strategy\'s own bankroll and its own run file, in the background, for `hours` (0.25-72) and/or `trades` closed trades (1-200), whichever comes first (default 24 h). Needs a backtest with the held-out split first. One per strategy, three at once. Cannot set a bankroll, a cap or a mode, and never runs real.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, hours: { type: 'number', minimum: 0.25, maximum: 72 }, trades: { type: 'integer', minimum: 1, maximum: 200 } }, required: ['id'], additionalProperties: false } },
319
+ { name: 'strategy_forward_stop', description: 'AUTOTRADE (paper): stop a running forward test (by strategy `id` or `forwardId`). It flattens at its next tick and writes its report; a test stopped early is shown but does not count as the forward-tested rung.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, forwardId: { type: 'string', maxLength: 40 } }, additionalProperties: false } },
320
+ { name: 'strategy_forward_report', description: 'AUTOTRADE (paper): a forward test (by strategy `id` or `forwardId`): state, window, progress and, once it ends, its numbers next to the backtest of the same rules (trades, win rate with its interval, net, drawdown, rug rate, fill cost drift) with plain verdict lines; it says when a sample is too small to claim anything. Read-only.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, forwardId: { type: 'string', maxLength: 40 } }, additionalProperties: false } },
321
+ { name: 'strategy_promote_propose', description: 'AUTOTRADE: PROPOSE promoting one of the user\'s strategies one rung up the ladder: `to` "always-on" (the always-on paper service; needs a finished forward test) or "real" (LIVE TRADE FIRE signals to the user\'s own executor; needs always-on first AND the user\'s evidence floor: a completed forward test of these exact rules with their minimum of closed paper trades, plus a backtest; below it nothing is proposed). Returns a proposal with an evidence card. It is NEVER applied by this tool and no rung can be skipped: the user applies always-on with a key, and real only with their own caps and a typed phrase on their own terminal. Cannot set caps, a bankroll or a mode.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, to: { type: 'string', enum: ['always-on', 'real'] }, why: { type: 'string', maxLength: 200 } }, required: ['id', 'to', 'why'], additionalProperties: false } },
314
322
  // ---- PARALLEL STRATEGIES (task-1529). Paper only. Two reads, and start / stop / pause of ONE strategy the user listed in
315
323
  // autotrade.json (a per-strategy control file). A tool can never add a strategy, change a bankroll or a cap, or go live.
316
324
  { name: 'autotrade_parallel_list', description: 'AUTOTRADE (paper): the strategies the user listed for parallel runs (autotrade.json: id, the preset it is based on, bankroll or shared paper account, enabled; the paper accounts several strategies trade from) and, while `dexterm autotrade run --all --paper` runs, each one\'s state (running / paused / halted / stopped), open positions, candidates and sessions. Read-only.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
@@ -320,16 +328,19 @@ const TOOLS_RAW = [
320
328
  // ---- TRADE LEDGER + neighbours (task-1530). Reads of the unified trade ledger (paper, desk, autotrade, live), a run's
321
329
  // record of a candidate, the recorder's outcome labels, the Telegram and live-signer STATE (never a key, an address or
322
330
  // an exact balance), and one PROPOSE tool: a backtest request the user runs or dismisses. None can trade or change a cap.
323
- { name: 'ledger_trades', description: 'TRADE LEDGER: trades from the unified ledger (one row per round trip, every source: paper, the agent paper desk, autotrade runs, the live signer), newest first: strategy id+version+hash, profile and entry reason (with where each came from: structured, rule, tag-line, late, default, missing), exit reason, size SOL/USD and % of book, entry/exit price AND market cap, fees, impact, slippage, hold time, MFE/MAE, rug flag, wallet mode, tx signatures (live), model session ids and cost. `fills: true` adds each fill. Read-only. PAPER rows are simulated: never present them as results.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' }, status: { type: 'string', enum: ['open', 'closed'] }, limit: { type: 'integer', minimum: 1, maximum: 200 }, fills: { type: 'boolean' } }, additionalProperties: false } },
324
- { name: 'ledger_positions', description: 'TRADE LEDGER: the open trades (cost, % of book, held time, profile, entry reason) from the unified ledger. No price fetch and no wallet balance: call get_token for a price. Read-only.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' }, limit: { type: 'integer', minimum: 1, maximum: 200 } }, additionalProperties: false } },
325
- { name: 'ledger_equity', description: 'TRADE LEDGER: the realized equity curve (cumulative P/L by close, in SOL when every trade has a SOL figure, else USD) with its max drawdown, at most 200 points. Read-only.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' } }, additionalProperties: false } },
326
- { name: 'ledger_summary', description: 'TRADE LEDGER: win rate with its 95% interval, P/L, profit factor, max drawdown, rug rate, average hold, MFE/MAE, fees, model cost, how many trades declared their profile / exit, and breakdowns by profile, exit reason, strategy, source and wallet mode. Read-only; small n means little.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' } }, additionalProperties: false } },
327
- { name: 'ledger_compare', description: 'TRADE LEDGER: one row per strategy version (id@version) side by side: trades, win rate with interval, P/L, drawdown, rug rate, hold, MFE/MAE, model cost. The forward-test table for strategies run in parallel. Read-only.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' } }, additionalProperties: false } },
331
+ { name: 'ledger_trades', description: 'TRADE LEDGER: trades from the unified ledger (one row per round trip, every source: paper, the agent paper desk, autotrade runs, the live signer), newest first: strategy id+version+hash, profile and entry reason (with where each came from: structured, rule, tag-line, late, default, missing), exit reason, size SOL/USD and % of book, entry/exit price AND market cap, fees, impact, slippage, hold time, MFE/MAE, rug flag, wallet mode, tx signatures (live), model session ids and cost. `fills: true` adds each fill. Read-only. PAPER rows are simulated: never present them as results.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live', 'real'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' }, status: { type: 'string', enum: ['open', 'closed'] }, limit: { type: 'integer', minimum: 1, maximum: 200 }, fills: { type: 'boolean' } }, additionalProperties: false } },
332
+ { name: 'ledger_positions', description: 'TRADE LEDGER: the open trades (cost, % of book, held time, profile, entry reason) from the unified ledger. No price fetch and no wallet balance: call get_token for a price. Read-only.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live', 'real'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' }, limit: { type: 'integer', minimum: 1, maximum: 200 } }, additionalProperties: false } },
333
+ { name: 'ledger_equity', description: 'TRADE LEDGER: the realized equity curve (cumulative P/L by close, in SOL when every trade has a SOL figure, else USD) with its max drawdown, at most 200 points. Read-only.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live', 'real'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' } }, additionalProperties: false } },
334
+ { name: 'ledger_summary', description: 'TRADE LEDGER: win rate with its 95% interval, P/L, profit factor, max drawdown, rug rate, average hold, MFE/MAE, fees, model cost, how many trades declared their profile / exit, and breakdowns by profile, exit reason, strategy, source and wallet mode. Read-only; small n means little.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live', 'real'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' } }, additionalProperties: false } },
335
+ { name: 'ledger_compare', description: 'TRADE LEDGER: one row per strategy version (id@version) side by side: trades, win rate with interval, P/L, drawdown, rug rate, hold, MFE/MAE, model cost. The forward-test table for strategies run in parallel. Read-only.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['paper', 'desk', 'autotrade', 'live'], description: 'Where the trade was booked: paper (the TUI ticket), desk (the agent paper desk), autotrade (a run), live (the signer).' }, mode: { type: 'string', enum: ['paper', 'devnet', 'live', 'real'] }, strategy: { type: 'string', maxLength: 50, description: 'A strategy id, id@version or id@hash-prefix; manual for trades with no strategy.' }, profile: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, exit_reason: { type: 'string', pattern: '^[a-z][a-z0-9-]{0,31}$' }, mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, account: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$', description: 'A shared paper account id (autotrade.json accounts): the trades of the strategies on it.' }, days: { type: 'number', minimum: 0, maximum: 3650, description: 'Only trades opened in the last N days.' } }, additionalProperties: false } },
328
336
  { name: 'candidate_dossier', description: 'AUTOTRADE: what a run recorded about one candidate coin: the entry rules it matched, the tick at the time, the session\'s decisions (action, profile, reason), the size clamp, the fills and how the trade closed. The newest run that saw the coin, or `run_id`. Read-only; for the coin as it is now call get_dossier.', inputSchema: { type: 'object', properties: { mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 } }, required: ['mint'], additionalProperties: false } },
329
337
  { name: 'outcomes_lookup', description: 'The recorder\'s outcome labels (rug, dead, fade, flat, runner, unobserved, by horizon, with the market-cap / liquidity / price ratios) for a coin (`mint`), or for every candidate of an autotrade run (`run_id`: what the entries and the skips turned into). Code-only labels from recorded numbers, not predictions. Read-only.', inputSchema: { type: 'object', properties: { mint: { type: 'string', pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' }, run_id: { type: 'string', maxLength: 64 }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, additionalProperties: false } },
330
338
  { name: 'telegram_status', description: 'The Telegram connector\'s settings: set up or not, enabled, how many chats are paired, the tiers (notify / control / approve), whether entries need a one-tap approval, the notify choices and quiet hours. Never the bot token or any chat or sender id. Read-only: only local commands change it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
331
- { name: 'trading_status', description: 'Where trading happens: dexterm trades on PAPER only and holds no key, signs nothing and places no live order (live trading happens in the Telegram trading bot the user chose), and whether the panic switch is engaged. Read-only.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
339
+ { name: 'trading_status', description: 'Where trading happens: dexterm trades on PAPER and holds no key, signs nothing and places no live order (live trading happens in the Telegram trading bot or script the user chose; in real mode dexterm sends it LIVE TRADE FIRE signals), whether the panic switch is engaged, and how many strategies the user armed for real mode. Read-only.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
340
+ { name: 'trade_fire_feed', description: 'Real mode\'s MCP channel: the LIVE TRADE FIRE signals (schema dexterm-trade-fire/1: buy/sell, mint, sanitized symbol, size in SOL and % of book, price context, exit reason, strategy, run, account, nonce, expiry) after `after_nonce`, plus real mode\'s status (armed strategies, kill switch, caps). For the user\'s own executor: act only on your accountId, ignore expired signals, dedupe by nonce. Read-only: no tool arms real mode, changes a cap or lifts the kill switch.', inputSchema: { type: 'object', properties: { after_nonce: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, additionalProperties: false } },
332
341
  { name: 'backtest_requests', description: 'The backtest requests filed with backtest_request, their status (pending, done, failed, dismissed) and the result line once the user ran one. Read-only.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
342
+ { name: 'trading_limits', description: 'Real mode\'s limits in force (task-1654) for a strategy / account / mode: every limit (sizing, risk, the optional sell rate, executor silence), its number, which scope set it (placeholder default, global, mode, strategy, account; the stricter wins), what it does, today\'s counters, the breaker and the executor\'s health. Read-only: explain them; to suggest a change use limits_propose. No tool changes a limit.', inputSchema: { type: 'object', properties: { strategy: { type: 'string', pattern: '^[a-z0-9][a-z0-9._-]{0,63}$' }, account: { type: 'string', pattern: '^[a-z0-9][a-z0-9._-]{0,63}$' }, mode: { type: 'string', enum: ['real', 'paper'] } }, additionalProperties: false } },
343
+ { name: 'limits_propose', description: 'PROPOSE a change to real mode\'s limits as a diff card: old -> new per limit, which ones it raises, and the exact `dexterm autotrade limits set` command for the user. It writes NOTHING and applies nothing: only the user can change a limit, on their own terminal, and raising one needs their typed confirmation. scope: global, real, paper, strategy:<id> or account:<id>; changes: { limitName: number, or null to remove/turn off }.', inputSchema: { type: 'object', properties: { scope: { type: 'string', maxLength: 80 }, changes: { type: 'object', additionalProperties: { type: ['number', 'null'] } }, why: { type: 'string', maxLength: 200 } }, required: ['changes'], additionalProperties: false } },
333
344
  { name: 'backtest_request', description: 'PROPOSE a longer backtest than strategy_backtest allows: a stored strategy `id` over the last `days` (1 to 60) of recorded history, optionally every candidate, with a one-line why. Writes a request the user runs (`dexterm autotrade requests run <id>`) or dismisses; nothing runs here. Read the result with backtest_requests. Cannot change a strategy, a cap or a mode.', inputSchema: { type: 'object', properties: { id: { type: 'string', pattern: '^[a-z][a-z0-9-]{1,31}$' }, days: { type: 'integer', minimum: 1, maximum: 60 }, every_candidate: { type: 'boolean' }, why: { type: 'string', maxLength: 200 } }, required: ['id', 'why'], additionalProperties: false } },
334
345
  ];
335
346
  /**
@@ -351,7 +362,7 @@ const PAPER_TOOLS = new Set([...PAPER_WRITE_TOOLS, ...PAPER_READ_TOOLS, ...PAPER
351
362
  * request (task-1530, they write a journal line / a request file). The only tools a skill may name
352
363
  * (task-1377); the paper read tools and the strategy read tools stay here (they cannot trade or change anything).
353
364
  */
354
- export const READ_ONLY_TOOLS = TOOLS.map((t) => String(t.name)).filter((n) => !ANALYST_ACTION_TYPES.includes(n) && !PAPER_WRITE_TOOLS.includes(n) && !STRATEGY_PROPOSE_TOOLS.includes(n) && !PARALLEL_CONTROL_TOOLS.includes(n) && !PAPER_NOTE_TOOLS.includes(n) && !LEDGER_PROPOSE_TOOLS.includes(n));
365
+ export const READ_ONLY_TOOLS = TOOLS.map((t) => String(t.name)).filter((n) => !ANALYST_ACTION_TYPES.includes(n) && !PAPER_WRITE_TOOLS.includes(n) && !STRATEGY_PROPOSE_TOOLS.includes(n) && !PARALLEL_CONTROL_TOOLS.includes(n) && !JOURNEY_CONTROL_TOOLS.includes(n) && !JOURNEY_PROPOSE_TOOLS.includes(n) && !PAPER_NOTE_TOOLS.includes(n) && !LEDGER_PROPOSE_TOOLS.includes(n));
355
366
  const SKILL_STORE = new OverlayStore({ kind: 'skill', bundled: [{ dir: SKILLS_DIR, layout: 'flat' }], readTools: READ_ONLY_TOOLS, home: () => dextermHome(process.env), onSkip: logSkip('skill') });
356
367
  /** Read the action allow-list the CLI put in screen_state.actions (defensive: any shape → empty lists). */
357
368
  function allowFrom(screenState) {
@@ -737,7 +748,7 @@ export class McpTools {
737
748
  if (PAPER_TOOLS.has(name))
738
749
  return this.paperTool(name, args);
739
750
  // task-1507: the AUTOTRADE strategy tools (reads + proposals; the user's keypress applies a proposal, never a tool)
740
- if (AUTOTRADE_TOOLS.includes(name) || PARALLEL_TOOLS.includes(name))
751
+ if (AUTOTRADE_TOOLS.includes(name) || PARALLEL_TOOLS.includes(name) || JOURNEY_TOOLS.includes(name))
741
752
  return (this.autotrade ??= new AutotradeTools({ actor: this.channel ? 'analyst' : 'mcp-agent', historyDb: () => this.historyDb() })).call(name, args);
742
753
  // task-1530: the trade ledger reads, a run's candidate record, outcomes, Telegram / trading STATE, backtest requests
743
754
  if (LEDGER_TOOLS.includes(name)) {
@@ -388,4 +388,11 @@ export const DEFAULT_KEYMAP = new Keymap([
388
388
  { keys: ['p'], action: 'autotrade.pause', label: 'pause new entries', screen: 'autotrade' },
389
389
  { keys: ['f'], action: 'autotrade.flatten', label: 'flatten (confirm)', screen: 'autotrade' },
390
390
  { keys: ['o'], action: 'autotrade.proposal', label: 'review chat proposal', screen: 'autotrade' },
391
+ // task-1647: a strategy's JOURNEY (draft → backtested → forward-tested → always-on → real). F starts a 24 h paper forward
392
+ // test (live data, the strategy's own bankroll), X stops it, A opens a pending always-on promotion (y applies). Real is
393
+ // never applied from a key: it needs the user's caps and a typed phrase on a terminal.
394
+ { keys: ['J'], action: 'autotrade.journey', label: 'journey (ladder)', screen: 'autotrade' },
395
+ { keys: ['F'], action: 'autotrade.forward', label: 'forward test (paper)', screen: 'autotrade' },
396
+ { keys: ['X'], action: 'autotrade.forwardstop', label: 'stop forward test', screen: 'autotrade' },
397
+ { keys: ['A'], action: 'autotrade.promote', label: 'review promotion', screen: 'autotrade' },
391
398
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dexterm-cli",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.dexterm-xyz/dexterm",
5
5
  "description": "dexterm: terminal DEX screener with a local Claude CLI analyst",
6
6
  "author": "The dexterm authors",