dexterm-cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agents/src/spectator.js +28 -2
- package/dist/packages/agents/src/tiers.js +3 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/autotrade/src/backtest.js +6 -2
- package/dist/packages/autotrade/src/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +2 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +132 -17
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +12 -1
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +220 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +18 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/app-help-gen.js +12 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +9 -2
- package/dist/packages/cli/src/autotrade-cmd/run.js +11 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/classifier-cmd.js +353 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/server.js +104 -0
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +5 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// dexterm autotrade — rug-proofing (task-1748): who can pull the pool, and a fast exit when it starts to go. PAPER ONLY.
|
|
2
|
+
//
|
|
3
|
+
// The control's two 2026-10-26 losses ($NOTHING, $AMERICA) and the three on 10-27 were creator LP pulls on hand-made
|
|
4
|
+
// PumpSwap pools: WSOL as the pool's BASE mint, launchpad unknown, the mint not ending in `pump`, the creator holding the
|
|
5
|
+
// LP tokens (FINDINGS-2026-10-27 §1-2). A pull is ONE transaction with no build-up: every post-entry signal (the pool's
|
|
6
|
+
// SOL vault, a Jupiter quote, DexScreener liquidity) fires only after it. So the PRIMARY defense is pre-entry and
|
|
7
|
+
// structural; the drain guard below only shortens the time a position sits in a pool that is being emptied and makes
|
|
8
|
+
// the book honest. A same-block creator pull cannot be front-run by anyone reading the chain.
|
|
9
|
+
//
|
|
10
|
+
// Pre-entry gates, cheapest first, fail closed (a gate that cannot be evaluated refuses the coin with `-unknown`):
|
|
11
|
+
// pool-age pool (pair) age >= minPoolAgeMin. Token age is a free lower bound first (a token younger
|
|
12
|
+
// than the minimum has no older pool); the pair's own creation time decides. tick, pair
|
|
13
|
+
// holders top-10 / dev share under the strategy's safety values (unknown = refuse) tick
|
|
14
|
+
// liquidity-recheck two liquidity reads >= afterSec apart: both >= floorX x the exit floor, the newer
|
|
15
|
+
// not more than maxDropPct under the older history
|
|
16
|
+
// pool-structure the pool is a pump.fun migration (PumpSwap, token = base, WSOL = quote) or a pool
|
|
17
|
+
// whose LP is verifiably burned / locked; WSOL as the BASE mint = hand-made: refuse pair
|
|
18
|
+
// lp-locked PumpSwap pool account -> lp mint: supply 0 (burned) passes; supply > 0 with the
|
|
19
|
+
// largest holder the pool creator (or any wallet over maxHolderLpPct) refuses RPC
|
|
20
|
+
// withdraw-history no `Withdraw` instruction among the pool's newest transactions (bounded scan) RPC
|
|
21
|
+
// sell-route a Jupiter token -> SOL quote at routeX x the position size returns a route Jupiter
|
|
22
|
+
// Every result has a named rule (`rugproof:<gate>[-why]`) that lands in the ledger; time gates (age, recheck) say
|
|
23
|
+
// when to look again instead of skipping the coin for good.
|
|
24
|
+
//
|
|
25
|
+
// Post-entry (drainVerdict): exit the whole position at once on liquidity / reserve down > liqDropPct from the entry
|
|
26
|
+
// read, a reserve near 0, or ANY Jupiter quote error (NO_ROUTES_FOUND, CANNOT_COMPUTE_…). A rate limit or a network
|
|
27
|
+
// failure is not a quote error (no exit on it; it is counted). Young or partly-verified pools get a smaller size
|
|
28
|
+
// (youngPoolSizeCapUsd).
|
|
29
|
+
//
|
|
30
|
+
// Pure: no I/O. The probes (DexScreener pair, PumpSwap pool account, LP mint, signatures, Jupiter) are in rugproof-probe.ts.
|
|
31
|
+
export const WSOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
32
|
+
export const PUMPSWAP_PROGRAM = 'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA';
|
|
33
|
+
export const RUGPROOF_DEFAULTS = {
|
|
34
|
+
minPoolAgeMin: 10,
|
|
35
|
+
requireSafePool: true,
|
|
36
|
+
maxHolderLpPct: 10,
|
|
37
|
+
maxWithdraws: 0,
|
|
38
|
+
withdrawScanTx: 25,
|
|
39
|
+
liquidityRecheck: { afterSec: 60, floorX: 2, maxDropPct: 15 },
|
|
40
|
+
routeX: 2,
|
|
41
|
+
youngPool: { underMin: 30, maxSizePct: 50 },
|
|
42
|
+
drainGuard: { pollSec: 10, liqDropPct: 25, exitOnNoRoute: true, minReserveSol: 0.5 },
|
|
43
|
+
};
|
|
44
|
+
export const GATE_ORDER = ['pool-age', 'holders', 'liquidity-recheck', 'pool-structure', 'lp-locked', 'withdraw-history', 'sell-route'];
|
|
45
|
+
const pass = (gate, detail, partial = false) => ({ gate, pass: true, rule: `rugproof:${gate}`, detail, ...(partial ? { partial: true } : {}) });
|
|
46
|
+
const fail = (gate, why, detail, retryAfterMs) => ({ gate, pass: false, rule: `rugproof:${gate}${why ? `-${why}` : ''}`, detail, ...(retryAfterMs !== undefined ? { retryAfterMs: Math.max(1000, Math.round(retryAfterMs)) } : {}) });
|
|
47
|
+
const m = (ms) => `${(ms / 60e3).toFixed(1)} min`;
|
|
48
|
+
const usd = (x) => `$${Math.round(x).toLocaleString('en-US')}`;
|
|
49
|
+
/** A pump.fun mint (vanity suffix) or Jupiter's launchpad tag says pump.fun. */
|
|
50
|
+
export const isPumpMint = (mint, launchpad) => mint.endsWith('pump') || launchpad === 'pump' || launchpad === 'pump.fun';
|
|
51
|
+
// ───────────────────────────── free gates (tick + history) ─────────────────────────────
|
|
52
|
+
/** Token age is a lower bound on pool age; the pair's creation time (when known) decides. */
|
|
53
|
+
export function gatePoolAge(r, k, pairCreatedAt, now = k.t) {
|
|
54
|
+
const min = r.minPoolAgeMin * 60e3;
|
|
55
|
+
if (k.createdAt !== null && now - k.createdAt < min)
|
|
56
|
+
return fail('pool-age', null, `token age ${m(now - k.createdAt)} < ${r.minPoolAgeMin} min (no older pool can exist)`, min - (now - k.createdAt));
|
|
57
|
+
if (pairCreatedAt === undefined)
|
|
58
|
+
return pass('pool-age', 'token age ok; pool age not read yet', true);
|
|
59
|
+
if (pairCreatedAt === null)
|
|
60
|
+
return fail('pool-age', 'unknown', 'the pool has no creation time');
|
|
61
|
+
const age = now - pairCreatedAt;
|
|
62
|
+
if (age < min)
|
|
63
|
+
return fail('pool-age', null, `pool age ${m(age)} < ${r.minPoolAgeMin} min`, min - age);
|
|
64
|
+
return pass('pool-age', `pool age ${m(age)}`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The strategy's own holder values (entry.safety, the GUARDRAILS numbers), with its own unknown rules: an unknown top-10
|
|
68
|
+
* share refuses unless unknownHolders is allow; an unknown dev share passes, as the engine's screen has always done (the
|
|
69
|
+
* holder sources rarely report it: refusing it would refuse nearly every coin), and is marked partial.
|
|
70
|
+
*/
|
|
71
|
+
export function gateHolders(k, maxTop10Pct, maxDevPct, unknownHolders = 'reject') {
|
|
72
|
+
let partial = false;
|
|
73
|
+
if (maxTop10Pct !== null) {
|
|
74
|
+
if (k.top10 === null) {
|
|
75
|
+
if (unknownHolders === 'reject')
|
|
76
|
+
return fail('holders', 'unknown', 'top-10 share unknown');
|
|
77
|
+
partial = true;
|
|
78
|
+
}
|
|
79
|
+
else if (k.top10 > maxTop10Pct)
|
|
80
|
+
return fail('holders', 'top10', `top 10 hold ${k.top10.toFixed(1)}% > ${maxTop10Pct}%`);
|
|
81
|
+
}
|
|
82
|
+
if (maxDevPct !== null && k.dev !== null && k.dev > maxDevPct)
|
|
83
|
+
return fail('holders', 'dev', `dev holds ${k.dev.toFixed(1)}% > ${maxDevPct}%`);
|
|
84
|
+
return pass('holders', `top10 ${k.top10 ?? '?'}%, dev ${k.dev ?? 'unknown'}%`, partial);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Two liquidity reads at least afterSec apart (the newest and the newest one at or before now - afterSec, from the
|
|
88
|
+
* coin's own tick history): both >= floorX x the exit floor, the newer not more than maxDropPct under the older.
|
|
89
|
+
*/
|
|
90
|
+
export function gateLiquidityRecheck(r, history, k, exitFloorUsd) {
|
|
91
|
+
const rc = r.liquidityRecheck;
|
|
92
|
+
if (!rc)
|
|
93
|
+
return pass('liquidity-recheck', 'not required');
|
|
94
|
+
const floor = rc.floorX * exitFloorUsd;
|
|
95
|
+
const liqNow = k.liq;
|
|
96
|
+
if (liqNow === null)
|
|
97
|
+
return fail('liquidity-recheck', 'unknown', 'no liquidity read');
|
|
98
|
+
if (liqNow < floor)
|
|
99
|
+
return fail('liquidity-recheck', 'floor', `liquidity ${usd(liqNow)} < ${rc.floorX}x the ${usd(exitFloorUsd)} exit floor`);
|
|
100
|
+
const cutoff = k.t - rc.afterSec * 1000;
|
|
101
|
+
let older = null;
|
|
102
|
+
for (const h of history)
|
|
103
|
+
if (h.mint === k.mint && h.t <= cutoff && h.liq !== null && (!older || h.t > older.t))
|
|
104
|
+
older = h;
|
|
105
|
+
if (!older) {
|
|
106
|
+
const first = history.find((h) => h.mint === k.mint && h.liq !== null);
|
|
107
|
+
const wait = first ? first.t + rc.afterSec * 1000 - k.t : rc.afterSec * 1000;
|
|
108
|
+
return fail('liquidity-recheck', 'pending', `no read ${rc.afterSec} s before this one yet`, wait > 0 ? wait : rc.afterSec * 1000);
|
|
109
|
+
}
|
|
110
|
+
if (older.liq < floor)
|
|
111
|
+
return fail('liquidity-recheck', 'floor', `liquidity ${usd(older.liq)} ${Math.round((k.t - older.t) / 1000)} s ago < ${rc.floorX}x the exit floor`);
|
|
112
|
+
const drop = (older.liq - liqNow) / older.liq * 100;
|
|
113
|
+
if (drop > rc.maxDropPct)
|
|
114
|
+
return fail('liquidity-recheck', 'falling', `liquidity ${usd(older.liq)} -> ${usd(liqNow)} in ${Math.round((k.t - older.t) / 1000)} s (-${drop.toFixed(1)}% > ${rc.maxDropPct}%)`);
|
|
115
|
+
return pass('liquidity-recheck', `${usd(older.liq)} -> ${usd(liqNow)} over ${Math.round((k.t - older.t) / 1000)} s`);
|
|
116
|
+
}
|
|
117
|
+
/** Owners that hold LP tokens without being able to withdraw them (burn / incinerator). Locks are not trusted by name. */
|
|
118
|
+
export const BURN_OWNERS = new Set(['1nc1nerator11111111111111111111111111111111', '11111111111111111111111111111111']);
|
|
119
|
+
export function gatePoolStructure(r, mint, launchpad, pair) {
|
|
120
|
+
if (!r.requireSafePool)
|
|
121
|
+
return pass('pool-structure', 'not required');
|
|
122
|
+
if (!pair)
|
|
123
|
+
return fail('pool-structure', 'unknown', 'no pool listed for the coin');
|
|
124
|
+
if (pair.baseMint === WSOL_MINT)
|
|
125
|
+
return fail('pool-structure', 'wsol-base', `WSOL is the pool's base mint (${pair.dex} ${pair.address.slice(0, 8)}…): a hand-made pool, not a pump.fun migration`);
|
|
126
|
+
if (pair.baseMint !== mint)
|
|
127
|
+
return fail('pool-structure', 'not-base', `the coin is not the pool's base mint (${pair.dex})`);
|
|
128
|
+
if (pair.quoteMint !== WSOL_MINT)
|
|
129
|
+
return fail('pool-structure', 'quote', `the pool's quote is not WSOL (${pair.quoteMint.slice(0, 8)}…)`);
|
|
130
|
+
const dex = pair.dex.toLowerCase();
|
|
131
|
+
if (dex === 'pumpswap') {
|
|
132
|
+
return isPumpMint(mint, launchpad)
|
|
133
|
+
? pass('pool-structure', 'pump.fun migration shape (PumpSwap, token = base, WSOL = quote)')
|
|
134
|
+
: pass('pool-structure', `PumpSwap, token = base, WSOL = quote, launchpad ${launchpad ?? 'unknown'}: LP must be verified`, true);
|
|
135
|
+
}
|
|
136
|
+
// Other venues (Raydium, Meteora, …): the LP lock cannot be verified here, so it is refused (fail closed).
|
|
137
|
+
return fail('pool-structure', 'unverifiable', `${pair.dex} pool: its LP lock cannot be verified by dexterm (only PumpSwap pools are read)`);
|
|
138
|
+
}
|
|
139
|
+
export function gateLpLocked(r, pair, lp, mint) {
|
|
140
|
+
if (!r.requireSafePool)
|
|
141
|
+
return pass('lp-locked', 'not required');
|
|
142
|
+
if (lp === 'error' || lp === null)
|
|
143
|
+
return fail('lp-locked', 'unknown', `the PumpSwap pool account ${pair.address.slice(0, 8)}… could not be read`);
|
|
144
|
+
if (lp.baseMint === WSOL_MINT)
|
|
145
|
+
return fail('lp-locked', 'wsol-base', 'the pool account says WSOL is the base mint (hand-made pool)');
|
|
146
|
+
if (lp.baseMint !== mint || lp.quoteMint !== WSOL_MINT)
|
|
147
|
+
return fail('lp-locked', 'mismatch', 'the pool account does not match the listed pair');
|
|
148
|
+
if (lp.lpSupply === 0n)
|
|
149
|
+
return pass('lp-locked', 'LP supply 0: every LP token is burned, nobody can withdraw');
|
|
150
|
+
const live = lp.holders.filter((h) => !BURN_OWNERS.has(h.owner) && h.amount > 0n);
|
|
151
|
+
const share = (a) => Number((a * 1000000n) / lp.lpSupply) / 10_000;
|
|
152
|
+
const byCreator = live.find((h) => h.owner === lp.creator);
|
|
153
|
+
if (byCreator)
|
|
154
|
+
return fail('lp-locked', 'creator', `the pool creator ${lp.creator.slice(0, 8)}… holds ${share(byCreator.amount).toFixed(2)}% of the LP: it can withdraw at any time`);
|
|
155
|
+
const top = live[0];
|
|
156
|
+
if (top && share(top.amount) > r.maxHolderLpPct)
|
|
157
|
+
return fail('lp-locked', 'holder', `one wallet ${top.owner.slice(0, 8)}… holds ${share(top.amount).toFixed(2)}% of the LP (> ${r.maxHolderLpPct}%)`);
|
|
158
|
+
if (!lp.holders.length)
|
|
159
|
+
return fail('lp-locked', 'unknown', 'LP supply > 0 but the holder list could not be read');
|
|
160
|
+
return pass('lp-locked', `LP supply > 0, largest live holder ${top ? share(top.amount).toFixed(2) : 0}%`, true);
|
|
161
|
+
}
|
|
162
|
+
export function gateWithdrawHistory(r, scan, lp) {
|
|
163
|
+
if (lp && lp.lpSupply === 0n && r.maxWithdraws === 0 && scan === null)
|
|
164
|
+
return pass('withdraw-history', 'LP burned: a withdraw is impossible');
|
|
165
|
+
if (scan === 'error' || scan === null)
|
|
166
|
+
return fail('withdraw-history', 'unknown', 'the pool\'s transactions could not be read');
|
|
167
|
+
if (scan.byCreator > 0)
|
|
168
|
+
return fail('withdraw-history', 'creator', `${scan.byCreator} creator Withdraw among the pool's newest ${scan.scanned} transactions`);
|
|
169
|
+
if (scan.withdraws > r.maxWithdraws)
|
|
170
|
+
return fail('withdraw-history', null, `${scan.withdraws} Withdraw among the pool's newest ${scan.scanned} transactions (max ${r.maxWithdraws})`);
|
|
171
|
+
return pass('withdraw-history', `no Withdraw among the newest ${scan.scanned} transactions`, scan.scanned < Math.min(5, r.withdrawScanTx));
|
|
172
|
+
}
|
|
173
|
+
export function gateSellRoute(r, route, sizeUsd) {
|
|
174
|
+
if (r.routeX === null)
|
|
175
|
+
return pass('sell-route', 'not required');
|
|
176
|
+
if (!route)
|
|
177
|
+
return fail('sell-route', 'unknown', 'no sell quote');
|
|
178
|
+
if (!route.ok)
|
|
179
|
+
return fail('sell-route', route.kind === 'no-route' ? 'none' : 'unknown', `Jupiter token -> SOL at ${r.routeX}x the ${usd(sizeUsd)} size: ${route.error}`);
|
|
180
|
+
return pass('sell-route', `Jupiter routes ${r.routeX}x the size (impact ${route.impactPct ?? '?'}%)`);
|
|
181
|
+
}
|
|
182
|
+
export function verdictOf(r, results, poolAgeMs) {
|
|
183
|
+
const refused = results.find((x) => !x.pass) ?? null;
|
|
184
|
+
const partial = results.some((x) => x.partial);
|
|
185
|
+
const young = r.youngPool && (poolAgeMs === null || poolAgeMs < r.youngPool.underMin * 60e3);
|
|
186
|
+
return { pass: !refused, results, refused, sizeCapPct: !refused && r.youngPool && (partial || young) ? r.youngPool.maxSizePct : null };
|
|
187
|
+
}
|
|
188
|
+
/** Exit the whole position at once on a reserve near 0, a reserve / liquidity drop > liqDropPct, or a quote error. */
|
|
189
|
+
export function drainVerdict(g, base, read) {
|
|
190
|
+
const ratio = base.reserveSol && read.reserveSol !== null ? read.reserveSol / base.reserveSol : null;
|
|
191
|
+
const estLiq = ratio !== null && base.liqUsd !== null ? base.liqUsd * ratio : read.liqUsd;
|
|
192
|
+
if (read.reserveSol !== null && read.reserveSol < g.minReserveSol)
|
|
193
|
+
return { exit: true, reason: 'drain-reserve', detail: `pool SOL reserve ${read.reserveSol.toFixed(3)} < ${g.minReserveSol} SOL`, liqUsd: estLiq ?? 0 };
|
|
194
|
+
if (ratio !== null && (1 - ratio) * 100 > g.liqDropPct)
|
|
195
|
+
return { exit: true, reason: 'drain-liquidity', detail: `pool SOL reserve ${base.reserveSol.toFixed(2)} -> ${read.reserveSol.toFixed(2)} (-${((1 - ratio) * 100).toFixed(1)}% > ${g.liqDropPct}%)`, liqUsd: estLiq };
|
|
196
|
+
if (base.liqUsd && read.liqUsd !== null && (1 - read.liqUsd / base.liqUsd) * 100 > g.liqDropPct)
|
|
197
|
+
return { exit: true, reason: 'drain-liquidity', detail: `liquidity ${usd(base.liqUsd)} -> ${usd(read.liqUsd)} (-${((1 - read.liqUsd / base.liqUsd) * 100).toFixed(1)}% > ${g.liqDropPct}% since entry)`, liqUsd: read.liqUsd };
|
|
198
|
+
if (g.exitOnNoRoute && read.route && !read.route.ok && read.route.kind === 'no-route')
|
|
199
|
+
return { exit: true, reason: 'drain-no-route', detail: `Jupiter has no sell route: ${read.route.error}`, liqUsd: estLiq ?? 0 };
|
|
200
|
+
if (read.route && !read.route.ok)
|
|
201
|
+
return { exit: false, note: `quote ${read.route.kind}: ${read.route.error}` };
|
|
202
|
+
return { exit: false };
|
|
203
|
+
}
|
|
204
|
+
/** The size a young or partly-verified pool may take: maxSizePct of what the strategy picked. */
|
|
205
|
+
export function youngPoolSizeCapUsd(v, usdPicked) {
|
|
206
|
+
return v.sizeCapPct === null ? null : (usdPicked * v.sizeCapPct) / 100;
|
|
207
|
+
}
|
|
208
|
+
// ───────────────────────────── offline (replays, backtests) ─────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* The gates a recorded tape can evaluate: pool age (the tape's createdAt: the token's, else the pair's), holders, the
|
|
211
|
+
* second liquidity read (the coin's own recorded rows; the recorder's ~2-min cadence makes afterSec at least one row
|
|
212
|
+
* gap), and the pool-structure PROXY: a pump.fun mint (`…pump` / launchpad pump) passes, anything else is refused as
|
|
213
|
+
* `rugproof:pool-structure-offline` (the tape has no pool account, LP mint or transactions). Used by backtest() for a
|
|
214
|
+
* rug-proof strategy and by the control's replay.
|
|
215
|
+
*/
|
|
216
|
+
export function rugproofTapeVet(r) {
|
|
217
|
+
return async (k, ctx) => {
|
|
218
|
+
const res = [
|
|
219
|
+
gatePoolAge(r, k, k.createdAt, k.t),
|
|
220
|
+
gateHolders(k, ctx?.maxTop10Pct ?? null, ctx?.maxDevPct ?? null, ctx?.unknownHolders),
|
|
221
|
+
// the structural proxy is free on a tape: it goes before the second read (a refusal for good beats a wait)
|
|
222
|
+
!r.requireSafePool || isPumpMint(k.mint, k.launchpad) ? pass('pool-structure', 'pump.fun mint (offline proxy)') : fail('pool-structure', 'offline', `not a pump.fun mint (launchpad ${k.launchpad}): the tape cannot verify its LP`),
|
|
223
|
+
gateLiquidityRecheck(r, ctx?.history ?? [], k, ctx?.exitFloorUsd ?? 2000),
|
|
224
|
+
];
|
|
225
|
+
const bad = res.find((x) => !x.pass);
|
|
226
|
+
if (bad)
|
|
227
|
+
return { ok: false, rule: bad.rule, detail: bad.detail, ...(bad.retryAfterMs !== undefined ? { retryAfterMs: bad.retryAfterMs } : {}) };
|
|
228
|
+
const v = verdictOf(r, res, k.createdAt !== null ? k.t - k.createdAt : null);
|
|
229
|
+
return { ok: true, recheck: true, ...(v.sizeCapPct !== null ? { sizeCapPct: v.sizeCapPct } : {}) };
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -34,6 +34,8 @@ export function simExecutor(x) {
|
|
|
34
34
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35
35
|
const REVIEW = ['time_review', 'time_tighten'];
|
|
36
36
|
const INFO_ONLY = ['mechanical_exit', 'stuck'];
|
|
37
|
+
/** Session ends that fail the same way every time: no new session for the rest of the run. */
|
|
38
|
+
const GUARD_REASONS = new Set(['account-mismatch', 'not-tool-less', 'account-unavailable']);
|
|
37
39
|
async function* fromArray(ticks) { for (const k of ticks)
|
|
38
40
|
yield k; }
|
|
39
41
|
export async function runAutotrade(o) {
|
|
@@ -121,27 +123,105 @@ export async function runAutotrade(o) {
|
|
|
121
123
|
const count = (rule) => { refusals[rule] = (refusals[rule] ?? 0) + 1; };
|
|
122
124
|
const refuse = (rule, data) => { count(rule); ledger.add(now, 'refused', { rule, ...data }); };
|
|
123
125
|
// A vet that throws or hangs is a check that could not run: the strategy's `unknown` decides (reject by default).
|
|
124
|
-
const vetCoin = async (k) => {
|
|
126
|
+
const vetCoin = async (k, ctx) => {
|
|
125
127
|
let r;
|
|
126
128
|
try {
|
|
127
|
-
r = await o.vet(k);
|
|
129
|
+
r = await o.vet(k, ctx);
|
|
128
130
|
}
|
|
129
131
|
catch (e) {
|
|
130
132
|
r = { ok: false, rule: 'vet-error', detail: e.message.slice(0, 160) };
|
|
131
133
|
}
|
|
132
134
|
if (r.ok) {
|
|
133
|
-
|
|
135
|
+
if (!r.recheck)
|
|
136
|
+
vetted.add(k.mint);
|
|
134
137
|
if (r.note)
|
|
135
|
-
ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200) });
|
|
138
|
+
ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200), ...(r.sizeCapPct !== undefined ? { sizeCapPct: r.sizeCapPct } : {}) });
|
|
136
139
|
}
|
|
137
140
|
return r;
|
|
138
141
|
};
|
|
142
|
+
const vetCtx = (k, sizeUsd, h) => ({ history: h, sizeUsd, exitFloorUsd: x.minExitLiquidityUsd, maxTop10Pct: s.entry.safety.maxTop10Pct, maxDevPct: s.entry.safety.maxDevPct, unknownHolders: s.entry.safety.unknownHolders });
|
|
143
|
+
const drainGuard = s.entry.safety.rugproof?.drainGuard ?? null;
|
|
144
|
+
const stuckSince = new Map();
|
|
145
|
+
/** The liquidity read each position was bought at (the drain guard's base). */
|
|
146
|
+
const entryLiq = new Map();
|
|
147
|
+
// Paper only: a real-signal bag is still in the user's wallet, so it keeps its exit signals and carries to the next
|
|
148
|
+
// real run (validateStrategy also refuses the option outside mode paper).
|
|
149
|
+
const writeOffMin = !o.real && s.mode === 'paper' ? x.stuckWriteOffMin ?? null : null;
|
|
150
|
+
/** Positions the pool probe saw drained (no feed-priced retry while it still says so). */
|
|
151
|
+
const drained = new Set();
|
|
152
|
+
/**
|
|
153
|
+
* task-1748, every step, for every open position:
|
|
154
|
+
* - the drain guard: an exit verdict from the pool poller (SOL vault / Jupiter, <= 10 s apart), or this coin's own
|
|
155
|
+
* feed liquidity down more than drainGuard.liqDropPct from the entry read, sells the whole position at once. The
|
|
156
|
+
* sale is priced on the liquidity the guard saw (0 when the route is gone), not on a lagging feed row: an
|
|
157
|
+
* emptied pool leaves nothing to sell into, the bag goes stuck and the write-off below closes it.
|
|
158
|
+
* - execution.stuckWriteOffMin (paper): a bag stuck that long is closed at its realizable value (sells already
|
|
159
|
+
* made kept, the rest 0), with a written_off event and a trade_closed line, so crash recovery never adopts it.
|
|
160
|
+
*/
|
|
161
|
+
const sweepOpen = async (tick) => {
|
|
162
|
+
for (const p of [...book.open.values()]) {
|
|
163
|
+
if (p.status !== 'open')
|
|
164
|
+
continue;
|
|
165
|
+
const k = latest.get(p.mint) ?? p.lastTick;
|
|
166
|
+
if (drainGuard && !p.stuck) {
|
|
167
|
+
const v = o.drain?.verdict(p.id) ?? null;
|
|
168
|
+
const base = entryLiq.has(p.id) ? entryLiq.get(p.id) ?? null : (p.liqWindow[0]?.liq || null);
|
|
169
|
+
const own = tick && tick.mint === p.mint && base && tick.liq !== null && (1 - tick.liq / base) * 100 > drainGuard.liqDropPct
|
|
170
|
+
? { reason: 'drain-liquidity', detail: `liquidity $${Math.round(base)} -> $${Math.round(tick.liq)} since entry (-${((1 - tick.liq / base) * 100).toFixed(1)}% > ${drainGuard.liqDropPct}%)`, liqUsd: tick.liq } : null;
|
|
171
|
+
const hit = v ?? own;
|
|
172
|
+
if (hit) {
|
|
173
|
+
ledger.add(now, 'drain_guard', { positionId: p.id, mint: p.mint, reason: hit.reason, detail: hit.detail, liqUsd: hit.liqUsd, source: v ? 'pool-probe' : 'feed' });
|
|
174
|
+
say(`DRAIN GUARD ${p.symbol}: ${hit.detail}`);
|
|
175
|
+
await sellPosition(p, { ...k, t: now, liq: hit.liqUsd ?? 0 }, p.tokens, hit.reason, hit.detail);
|
|
176
|
+
// The probe saw the pool emptied or the route gone: the lagging feed must not "sell" the bag at its old
|
|
177
|
+
// liquidity. While the probe still says so, the monitor's stuck retry is off for this position.
|
|
178
|
+
if (v && p.status === 'open' && p.stuck)
|
|
179
|
+
drained.add(p.id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const since = stuckSince.get(p.id);
|
|
183
|
+
if (writeOffMin !== null && p.status === 'open' && p.stuck && since !== undefined && now - since >= writeOffMin * 60e3) {
|
|
184
|
+
const ct = book.close(p, now, 'written-off', true);
|
|
185
|
+
o.drain?.unwatch(p.id);
|
|
186
|
+
stuckSince.delete(p.id);
|
|
187
|
+
ledger.add(now, 'written_off', { positionId: p.id, mint: p.mint, stuckMin: Math.round((now - since) / 6e3) / 10, realizedUsd: ct.proceedsUsd, costUsd: ct.costUsd, why: `stuck ${writeOffMin} min under the exit floor: closed at realizable value (the rest counts 0)` });
|
|
188
|
+
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, writtenOff: true });
|
|
189
|
+
say(`WRITTEN OFF ${p.symbol}: stuck ${writeOffMin} min, ${ct.pnlPct}% realized`);
|
|
190
|
+
}
|
|
191
|
+
if (p.status === 'closed') {
|
|
192
|
+
const c = byPosition.get(p.id);
|
|
193
|
+
if (c) {
|
|
194
|
+
if (c.inflight)
|
|
195
|
+
await c.inflight;
|
|
196
|
+
await closeCandidate(c, 'position-closed');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
139
201
|
const liveCandidateSessions = () => [...cands.values()].filter((q) => q.session && !q.positionId).length;
|
|
140
202
|
const everyLive = () => [...new Set([...cands.values(), ...byPosition.values()])];
|
|
141
203
|
const newCand = (k, matched, session, kind, positionId) => ({
|
|
142
204
|
mint: k.mint, symbol: k.symbol, since: now, lastMatch: now, matched, session, sessionKind: kind, lastSent: view(k), lastSentT: now, seq: 0,
|
|
143
205
|
inflight: null, pendingText: [], notes: [], closing: false, positionId, awaitingReview: null, oneShot: kind === 'position', costSeen: 0, turns: 0, lastDecision: null,
|
|
144
206
|
});
|
|
207
|
+
/**
|
|
208
|
+
* Why the session's process ended on its own (null while alive). A session that proved the account or its tool list
|
|
209
|
+
* wrong (or had no account) blocks new sessions for the run; the guard is logged once, by the session that set it.
|
|
210
|
+
* Checked on every path that learns of a death: a null reply AND a close (a candidate's TTL can close a session that
|
|
211
|
+
* already died, before its null reply is read; task-1712).
|
|
212
|
+
*/
|
|
213
|
+
let guardLogged = false;
|
|
214
|
+
const sessionDied = (c) => {
|
|
215
|
+
const died = c.session?.endReason?.() ?? null;
|
|
216
|
+
if (c.session && died && GUARD_REASONS.has(died)) {
|
|
217
|
+
if (!guardLogged && (gate.blocked === null || gate.blockedBy === c.session.id)) {
|
|
218
|
+
guardLogged = true;
|
|
219
|
+
ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
|
|
220
|
+
}
|
|
221
|
+
gate.block(died, c.session.id);
|
|
222
|
+
}
|
|
223
|
+
return died;
|
|
224
|
+
};
|
|
145
225
|
const closeCandidate = async (c, reason) => {
|
|
146
226
|
if (c.closing)
|
|
147
227
|
return;
|
|
@@ -163,8 +243,11 @@ export async function runAutotrade(o) {
|
|
|
163
243
|
c.costSeen = st.costUsd;
|
|
164
244
|
}
|
|
165
245
|
sessionCost += st.costUsd;
|
|
166
|
-
|
|
167
|
-
|
|
246
|
+
// A session that had already failed its proof ended for that reason, whatever closed its candidate.
|
|
247
|
+
const died = sessionDied(c);
|
|
248
|
+
const why = died && GUARD_REASONS.has(died) ? died : reason;
|
|
249
|
+
ledger.add(now, 'session_end', { sessionId: c.session.id, sessionKind: c.sessionKind, mint: c.mint, reason: why, turns: st.turns, costUsd: st.costUsd });
|
|
250
|
+
say(`session ${c.session.id} ended (${why}) · ${st.turns} turns · $${st.costUsd.toFixed(4)}`);
|
|
168
251
|
}
|
|
169
252
|
};
|
|
170
253
|
const sellPosition = async (p, k, tokens, reason, detail) => {
|
|
@@ -174,21 +257,37 @@ export async function runAutotrade(o) {
|
|
|
174
257
|
if ('refused' in f) {
|
|
175
258
|
if (!p.stuck) {
|
|
176
259
|
p.stuck = true;
|
|
260
|
+
stuckSince.set(p.id, now);
|
|
177
261
|
ledger.add(now, 'stuck', { positionId: p.id, mint: p.mint, why: f.refused, reason });
|
|
178
262
|
say(`STUCK ${p.symbol}: cannot sell (${f.refused})`);
|
|
179
263
|
}
|
|
180
264
|
return;
|
|
181
265
|
}
|
|
182
266
|
p.stuck = false;
|
|
267
|
+
stuckSince.delete(p.id);
|
|
183
268
|
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), detail, ...sigTag(f) });
|
|
184
269
|
say(`SELL ${p.symbol} ${reason}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)} (impact ${f.impactPct}%) — ${detail}`);
|
|
185
270
|
const ct = book.sell(p, f);
|
|
271
|
+
if (ct)
|
|
272
|
+
o.drain?.unwatch(p.id);
|
|
186
273
|
if (ct) {
|
|
187
274
|
// task-1530: the price extremes the monitor saw while held (the trade ledger's MFE / MAE)
|
|
188
275
|
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, ...(o.real ? { tag: 'real-signal' } : {}) });
|
|
189
276
|
say(`CLOSED ${ct.symbol}: ${ct.pnlUsd >= 0 ? '+' : ''}$${ct.pnlUsd.toFixed(2)} (${ct.pnlPct}%) by ${ct.exitReason}`);
|
|
190
277
|
}
|
|
191
278
|
};
|
|
279
|
+
/** task-1748: a young or partly verified pool takes at most the vet's sizeCapPct of the strategy's pick. */
|
|
280
|
+
const capYoungPool = (r, c) => {
|
|
281
|
+
if (!r.ok || c.sizeCapPct === undefined)
|
|
282
|
+
return r;
|
|
283
|
+
const capUsd = Math.round(r.usd * c.sizeCapPct) / 100;
|
|
284
|
+
if (capUsd >= r.usd)
|
|
285
|
+
return r;
|
|
286
|
+
if (capUsd < s.sizing.minUsd)
|
|
287
|
+
return { ok: false, refused: 'below-min-after-rugproof-young-pool', clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
|
|
288
|
+
const f = r.usd ? capUsd / r.usd : 1;
|
|
289
|
+
return { ...r, usd: capUsd, firstTrancheUsd: Math.round(r.firstTrancheUsd * f * 100) / 100, clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
|
|
290
|
+
};
|
|
192
291
|
/** Apply a validated decision at the current virtual time. */
|
|
193
292
|
const apply = async (c, d) => {
|
|
194
293
|
const k = latest.get(c.mint);
|
|
@@ -213,7 +312,7 @@ export async function runAutotrade(o) {
|
|
|
213
312
|
return;
|
|
214
313
|
}
|
|
215
314
|
const req = { profile: d.profile, sizePct: d.sizePct, sizeUsd: d.sizeUsd, confidence: d.confidence };
|
|
216
|
-
const r = clampEntry(book, k, req, killed);
|
|
315
|
+
const r = capYoungPool(clampEntry(book, k, req, killed), c);
|
|
217
316
|
ledger.add(now, 'clamp', { mint: c.mint, sessionId: c.session?.id ?? null, request: req, result: r.ok ? { ok: true, usd: r.usd, firstTrancheUsd: r.firstTrancheUsd, profile: r.profile.id, basis: r.basis, clamps: r.clamps } : r });
|
|
218
317
|
for (const cl of r.clamps)
|
|
219
318
|
clampCount[cl.rule] = (clampCount[cl.rule] ?? 0) + 1;
|
|
@@ -356,14 +455,9 @@ export async function runAutotrade(o) {
|
|
|
356
455
|
c.costSeen = Math.max(c.costSeen, r.costUsd);
|
|
357
456
|
}
|
|
358
457
|
if (!r || !r.decision.ok) {
|
|
359
|
-
const died = c
|
|
458
|
+
const died = sessionDied(c);
|
|
360
459
|
if (!r)
|
|
361
460
|
ledger.add(now, 'session_timeout', { sessionId: c.session.id, mint: c.mint, ended: died });
|
|
362
|
-
if (died === 'account-mismatch' || died === 'not-tool-less' || died === 'account-unavailable') {
|
|
363
|
-
if (!gate.blocked)
|
|
364
|
-
ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
|
|
365
|
-
gate.block(died);
|
|
366
|
-
}
|
|
367
461
|
if (review)
|
|
368
462
|
await noAnswer(review, !r ? (died ?? 'timeout') : 'invalid-decision');
|
|
369
463
|
if (!r) {
|
|
@@ -477,6 +571,8 @@ export async function runAutotrade(o) {
|
|
|
477
571
|
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, profile: prof.id, ...fillMcaps(fillView(f), k), bookUsd, ...(q.ctx?.proposalId ? { proposalId: q.ctx.proposalId } : {}), ...sigTag(f) });
|
|
478
572
|
say(`BUY ${k.symbol} [${prof.id}]: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}${f.partial ? ' (PARTIAL: impact cap)' : ''} · liq $${Math.round(k.liq ?? 0)}`);
|
|
479
573
|
const c = cands.get(q.mint);
|
|
574
|
+
entryLiq.set(p.id, k.liq ?? null);
|
|
575
|
+
o.drain?.watch({ positionId: p.id, mint: p.mint, entryLiqUsd: k.liq ?? null, poolAddress: c?.pool?.address ?? null, quoteVault: c?.pool?.quoteVault ?? null, tokens: () => p.tokens });
|
|
480
576
|
if (c) {
|
|
481
577
|
c.positionId = p.id;
|
|
482
578
|
byPosition.set(p.id, c);
|
|
@@ -617,6 +713,7 @@ export async function runAutotrade(o) {
|
|
|
617
713
|
ledger.add(now, 'halt', { reason: book.state.halted });
|
|
618
714
|
o.approvals?.cancelAll(`halted:${book.state.halted}`);
|
|
619
715
|
}
|
|
716
|
+
await sweepOpen(k);
|
|
620
717
|
// Held coin → the monitor (mechanical first, then wakes).
|
|
621
718
|
const pos = book.open.get(k.mint);
|
|
622
719
|
const chatSell = o.approvals?.takeExit?.(k.mint) ?? null;
|
|
@@ -646,7 +743,11 @@ export async function runAutotrade(o) {
|
|
|
646
743
|
}
|
|
647
744
|
}
|
|
648
745
|
}
|
|
746
|
+
else if (pos && drained.has(pos.id) && o.drain?.verdict(pos.id)) {
|
|
747
|
+
// task-1748: still drained per the pool probe: nothing sells into an emptied pool (the write-off closes it).
|
|
748
|
+
}
|
|
649
749
|
else if (pos) {
|
|
750
|
+
drained.delete(pos.id);
|
|
650
751
|
const prof0 = s.exits.profiles[pos.profileId];
|
|
651
752
|
// task-1654: a real run's (or a paper scope's) maxHoldMinutes limit tightens the profile's holding cap (never loosens it).
|
|
652
753
|
const holdCap = o.real?.maxHoldMinutes?.() ?? o.maxHoldMinutes?.() ?? null;
|
|
@@ -707,18 +808,26 @@ export async function runAutotrade(o) {
|
|
|
707
808
|
if (sr.pass) {
|
|
708
809
|
// Cheap pre-check so a full book does not spawn sessions it could never act on.
|
|
709
810
|
const pre = clampEntry(book, k, {}, killed);
|
|
710
|
-
const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k) : null;
|
|
811
|
+
const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k, vetCtx(k, pre.usd, h)) : null;
|
|
711
812
|
if (!pre.ok) {
|
|
712
813
|
if (!['already-open'].includes(pre.refused))
|
|
713
814
|
count(`precheck:${pre.refused}`);
|
|
714
815
|
}
|
|
715
816
|
else if (v && !v.ok) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
817
|
+
// task-1748: a time gate (pool age, the second liquidity read) looks again later; any other refusal is for good.
|
|
818
|
+
const retry = v.retryAfterMs !== undefined;
|
|
819
|
+
refuse(v.rule, { mint: k.mint, symbol: k.symbol, stage: 'vet', detail: v.detail, ...(retry ? { retryAfterMs: v.retryAfterMs } : {}) });
|
|
820
|
+
skippedUntil.set(k.mint, retry ? now + v.retryAfterMs : Infinity);
|
|
821
|
+
say(`SKIP ${k.symbol} (${k.mint.slice(0, 4)}…): ${v.rule} (${v.detail})${retry ? `; again in ${Math.round(v.retryAfterMs / 1000)} s` : ''}`);
|
|
719
822
|
}
|
|
720
823
|
else if (decideAuto) {
|
|
721
824
|
const cand = newCand(k, sr.matched, null, 'candidate', null);
|
|
825
|
+
if (v?.ok) {
|
|
826
|
+
if (v.sizeCapPct !== undefined)
|
|
827
|
+
cand.sizeCapPct = v.sizeCapPct;
|
|
828
|
+
if (v.pool)
|
|
829
|
+
cand.pool = v.pool;
|
|
830
|
+
}
|
|
722
831
|
cands.set(k.mint, cand);
|
|
723
832
|
ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
|
|
724
833
|
await apply(cand, { action: 'enter', reason: 'auto' });
|
|
@@ -734,6 +843,12 @@ export async function runAutotrade(o) {
|
|
|
734
843
|
const session = o.sessions(sid, s, { kind: 'candidate', mint: k.mint });
|
|
735
844
|
gate.opened('candidate');
|
|
736
845
|
const cand = newCand(k, sr.matched, session, 'candidate', null);
|
|
846
|
+
if (v?.ok) {
|
|
847
|
+
if (v.sizeCapPct !== undefined)
|
|
848
|
+
cand.sizeCapPct = v.sizeCapPct;
|
|
849
|
+
if (v.pool)
|
|
850
|
+
cand.pool = v.pool;
|
|
851
|
+
}
|
|
737
852
|
cands.set(k.mint, cand);
|
|
738
853
|
ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
|
|
739
854
|
ledger.add(now, 'session_start', { sessionId: sid, sessionKind: 'candidate', agent: session.kind, mint: k.mint, model: sessionModel(session, s), budgetUsd: gate.caps.budgetUsdPerSession });
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// of its path, so a test home never replaces (or removes) the real one. A non-default home is passed as `--home`.
|
|
20
20
|
// Only PATH is copied into the job's environment (the service needs to find `claude`); nothing else, no secrets.
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
|
-
import { join } from 'node:path';
|
|
22
|
+
import { join, posix } from 'node:path';
|
|
23
23
|
export const SERVICE_LABEL = 'xyz.dexterm.autotrade';
|
|
24
24
|
export const INSTALL_MANIFEST_SCHEMA = 'dexterm-autotrade-service-install/1';
|
|
25
25
|
/** The label for a DEXTERM_HOME: the default one, or one suffixed by a hash of the path. */
|
|
@@ -32,7 +32,9 @@ export function serviceLabel(dexHome, defaultHome) {
|
|
|
32
32
|
export const ensureArgs = (s) => [s.nodePath, s.binPath, 'autotrade', 'service', 'ensure', '--quiet', ...(s.dexHome ? ['--home', s.dexHome] : [])];
|
|
33
33
|
// ───────────────────────────── macOS ─────────────────────────────
|
|
34
34
|
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
35
|
-
|
|
35
|
+
// LaunchAgent and systemd paths are macOS / Linux paths whatever host computes the plan (task-1712: a Windows host
|
|
36
|
+
// joined them with `\`); the Windows task and the manifest live under the local dexHome and use the host's join.
|
|
37
|
+
export const launchAgentPath = (s) => posix.join(s.userHome, 'Library', 'LaunchAgents', `${s.label}.plist`);
|
|
36
38
|
export function renderLaunchAgent(s) {
|
|
37
39
|
const args = ensureArgs(s).map((a) => ` <string>${xml(a)}</string>`).join('\n');
|
|
38
40
|
const env = s.path ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${xml(s.path)}</string>\n </dict>\n` : '';
|
|
@@ -70,7 +72,7 @@ ${env} <key>StartInterval</key>
|
|
|
70
72
|
}
|
|
71
73
|
// ───────────────────────────── Linux (systemd user) ─────────────────────────────
|
|
72
74
|
export const systemdUnit = (s) => s.label.replace(/^xyz\./, '').replace(/\./g, '-');
|
|
73
|
-
export const systemdDir = (s) => join(s.userHome, '.config', 'systemd', 'user');
|
|
75
|
+
export const systemdDir = (s) => posix.join(s.userHome, '.config', 'systemd', 'user');
|
|
74
76
|
/** systemd quoting: double quotes, backslash and quote escaped, `%` doubled (specifier expansion). */
|
|
75
77
|
const sd = (a) => `"${a.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
|
|
76
78
|
export function renderSystemdService(s) {
|
|
@@ -189,7 +191,7 @@ export function installPlan(s, dexHome) {
|
|
|
189
191
|
const dir = systemdDir(s);
|
|
190
192
|
return {
|
|
191
193
|
platform: s.platform, label: s.label, name: `${unit}.timer`,
|
|
192
|
-
files: [{ path: join(dir, `${unit}.service`), content: renderSystemdService(s) }, { path: join(dir, `${unit}.timer`), content: renderSystemdTimer(s) }],
|
|
194
|
+
files: [{ path: posix.join(dir, `${unit}.service`), content: renderSystemdService(s) }, { path: posix.join(dir, `${unit}.timer`), content: renderSystemdTimer(s) }],
|
|
193
195
|
activate: [['systemctl', '--user', 'daemon-reload'], ['systemctl', '--user', 'enable', '--now', `${unit}.timer`]],
|
|
194
196
|
deactivate: [['systemctl', '--user', 'disable', '--now', `${unit}.timer`]],
|
|
195
197
|
notes: ['A systemd user timer runs while you are logged in; `loginctl enable-linger` (your choice) keeps it running after logout and from boot.'],
|
|
@@ -131,12 +131,10 @@ class ClaudeCliSession {
|
|
|
131
131
|
tools: iso.tools, mcpServers: iso.mcpServers, plugins: iso.plugins, agents: iso.agents, skills: iso.skills, slashCommands: iso.slashCommands,
|
|
132
132
|
};
|
|
133
133
|
this.o.onInit?.(init);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (init.tools !== 0 || init.mcpServers !== 0) {
|
|
139
|
-
this.kill('not-tool-less');
|
|
134
|
+
const failed = !this.o.account.expectApiKeySource.includes(init.apiKeySource) ? 'account-mismatch' : init.tools !== 0 || init.mcpServers !== 0 ? 'not-tool-less' : null;
|
|
135
|
+
if (failed) {
|
|
136
|
+
this.o.onProofFailed?.(failed);
|
|
137
|
+
this.kill(failed);
|
|
140
138
|
return;
|
|
141
139
|
}
|
|
142
140
|
continue;
|
|
@@ -229,6 +227,8 @@ export class SessionGate {
|
|
|
229
227
|
pausedUntil = 0;
|
|
230
228
|
/** Set when a session proved the account or the tool list wrong: no new session until the runner restarts. */
|
|
231
229
|
blocked = null;
|
|
230
|
+
/** The session whose own failure set `blocked` (null when the block came from elsewhere, e.g. a sibling strategy). */
|
|
231
|
+
blockedBy = null;
|
|
232
232
|
constructor(caps, guard, opts = {}) {
|
|
233
233
|
this.caps = caps;
|
|
234
234
|
this.guard = guard;
|
|
@@ -312,7 +312,12 @@ export class SessionGate {
|
|
|
312
312
|
/** Are new sessions paused by the allowance guard right now (wall clock)? */
|
|
313
313
|
paused() { return this.clock() < this.pausedUntil; }
|
|
314
314
|
/** A misconfigured account (or a CLI that reports tools) fails the same way every time: stop spawning for this run. */
|
|
315
|
-
block(reason) {
|
|
315
|
+
block(reason, sessionId) {
|
|
316
|
+
if (this.blocked !== null)
|
|
317
|
+
return;
|
|
318
|
+
this.blocked = reason;
|
|
319
|
+
this.blockedBy = sessionId ?? null;
|
|
320
|
+
}
|
|
316
321
|
}
|
|
317
322
|
const isRec = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
318
323
|
/** The MCP-shaped sections a replay can honestly fill from the tape row; everything else is named as unavailable. */
|
|
@@ -455,6 +460,7 @@ export function claudeSessionFactory(f) {
|
|
|
455
460
|
catch (e) {
|
|
456
461
|
failed = 'account-unavailable';
|
|
457
462
|
f.log('session_account_error', { sessionId: id, error: e.message });
|
|
463
|
+
f.gate.block(failed, id);
|
|
458
464
|
return null;
|
|
459
465
|
}
|
|
460
466
|
if (closed)
|
|
@@ -467,6 +473,9 @@ export function claudeSessionFactory(f) {
|
|
|
467
473
|
onRateLimit: (r) => { f.gate.rateLimit(r); f.log('allowance', { sessionId: id, status: r.status, fiveHour: r.fiveHour, sevenDay: r.sevenDay, resetsAt: r.resetsAt }); },
|
|
468
474
|
onLate: (text) => f.log('late_reply_ignored', { sessionId: id, why: 'arrived after decideWithinSec', text: text.slice(0, 400) }),
|
|
469
475
|
onRaw: f.onRaw ? (line) => f.onRaw(id, line) : undefined,
|
|
476
|
+
// The gate closes at the failed proof itself, not when the runner later reads this session's (null) reply: a
|
|
477
|
+
// candidate that closes first (its TTL) must not leave the gate open for more sessions on the wrong account.
|
|
478
|
+
onProofFailed: (why) => f.gate.block(why, id),
|
|
470
479
|
});
|
|
471
480
|
return inner;
|
|
472
481
|
})());
|