dexterm-cli 0.2.0 → 0.2.2
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/chat/connector.js +5 -4
- package/dist/packages/autotrade/src/chat/control.js +3 -0
- package/dist/packages/autotrade/src/chat/hub.js +37 -2
- package/dist/packages/autotrade/src/chat/index.js +3 -2
- package/dist/packages/autotrade/src/chat/readonly.js +530 -0
- package/dist/packages/autotrade/src/chat/service.js +12 -1
- package/dist/packages/autotrade/src/engine.js +75 -29
- package/dist/packages/autotrade/src/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +4 -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 +203 -30
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/store.js +1 -1
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +29 -1
- package/dist/packages/autotrade/src/tiers.js +194 -0
- 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 +298 -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 +19 -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/classifier/src/win-acl.js +71 -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 +13 -1
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +12 -3
- package/dist/packages/cli/src/autotrade-cmd/run.js +14 -2
- package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
- package/dist/packages/cli/src/classifier-cmd.js +361 -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/skill-eval-build.js +7 -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/autotrade-telegram.md +1 -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-confidence-tiers-experimental-unproven-paper-only.md +56 -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 +36 -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-read-only-data-check-top-and-signals-experimental.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.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 +106 -1
- 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/packs/src/skill-care.js +10 -2
- 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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// three independent fences, never the only one.
|
|
13
13
|
import { parseFilter } from "../../cli/src/screener/filter.js";
|
|
14
14
|
import { tickRow } from "./replay.js";
|
|
15
|
+
import { TIERS_HARD_CAP_PCT, selectConfidenceTier, tierTable } from "./tiers.js";
|
|
15
16
|
export const DAY_MS = 86_400_000;
|
|
16
17
|
const r2 = (x) => Math.round(x * 100) / 100;
|
|
17
18
|
const r6 = (x) => Number(x.toPrecision(6));
|
|
@@ -297,7 +298,7 @@ export const dayOf = (t) => Math.floor(t / DAY_MS);
|
|
|
297
298
|
* cooldowns, counts), then the size the strategy itself picks, then every ceiling, each reduction recorded.
|
|
298
299
|
* An agent request can only LOWER the size below the strategy's own pick, never raise it above a ceiling.
|
|
299
300
|
*/
|
|
300
|
-
export function clampEntry(book, k, req, killSwitch = false) {
|
|
301
|
+
export function clampEntry(book, k, req, killSwitch = false, nowMs = k.t, inFlight) {
|
|
301
302
|
const s = book.strategy;
|
|
302
303
|
const st = book.state;
|
|
303
304
|
const clamps = [];
|
|
@@ -306,6 +307,11 @@ export function clampEntry(book, k, req, killSwitch = false) {
|
|
|
306
307
|
// fills become trade-fire signals for the user's own executor (the runner admits it only through the real-mode hook).
|
|
307
308
|
if (s.mode !== 'paper' && s.mode !== 'real')
|
|
308
309
|
return no('mode-not-paper');
|
|
310
|
+
// 0.2.2: EXPERIMENTAL confidence tiers are PAPER ONLY. validateStrategy and the runner refuse them too; this is the
|
|
311
|
+
// engine's own fence for any document that reached it another way.
|
|
312
|
+
const tiersOn = s.sizing.tiers ?? null;
|
|
313
|
+
if (tiersOn && s.mode !== 'paper')
|
|
314
|
+
return no('tiers-paper-only');
|
|
309
315
|
if (killSwitch)
|
|
310
316
|
return no('kill-switch');
|
|
311
317
|
if (st.halted)
|
|
@@ -339,34 +345,57 @@ export function clampEntry(book, k, req, killSwitch = false) {
|
|
|
339
345
|
}
|
|
340
346
|
if ((k.liq ?? 0) < profile.fit.minLiquidityUsd)
|
|
341
347
|
return no('profile-min-liquidity');
|
|
348
|
+
// 0.2.2 confidence tiers: score the tick (fresh at nowMs), pick the tier; T0 is a skip; one open position unless the
|
|
349
|
+
// block says not. With tiers, an entry decided but not filled yet (the latency window, an open approval) counts as
|
|
350
|
+
// open for one-open and maxConcurrent, and its dollars come off the exposure and cash room (task-2156).
|
|
351
|
+
let tier;
|
|
352
|
+
const fly = tiersOn ? inFlightOf(inFlight) : { opens: 0, usd: 0 };
|
|
353
|
+
if (tiersOn) {
|
|
354
|
+
if (tiersOn.oneOpen !== false && book.open.size + fly.opens >= 1)
|
|
355
|
+
return no('tier-one-open');
|
|
356
|
+
if (book.open.size - bags + fly.opens >= s.risk.maxConcurrent)
|
|
357
|
+
return no('max-concurrent');
|
|
358
|
+
tier = selectConfidenceTier(k, tierTable(tiersOn.table), nowMs);
|
|
359
|
+
if (tier.tier === 'T0')
|
|
360
|
+
return { ok: false, refused: 'tier-T0', clamps, tier };
|
|
361
|
+
}
|
|
342
362
|
const bank = book.bookEquity();
|
|
343
363
|
const z = s.sizing;
|
|
344
364
|
let usd;
|
|
345
365
|
let basis;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
basis = `${z.pct}% of $${r2(bank)}`;
|
|
354
|
-
break;
|
|
355
|
-
case 'risk': {
|
|
356
|
-
const stop = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
|
|
357
|
-
usd = (bank * (z.riskPct ?? 0)) / stop;
|
|
358
|
-
basis = `${z.riskPct}% risk / ${stop}% stop of $${r2(bank)}`;
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
case 'confidence': {
|
|
362
|
-
const c = z.confidence ?? { minPct: 1, maxPct: 1, default: 0.5 };
|
|
363
|
-
const conf = clamp01(req.confidence ?? c.default);
|
|
364
|
-
const pct = c.minPct + (c.maxPct - c.minPct) * conf;
|
|
365
|
-
usd = (bank * pct) / 100;
|
|
366
|
-
basis = `confidence ${r2(conf)} → ${r2(pct)}% of $${r2(bank)}`;
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
366
|
+
// With tiers on, the tier's runs-at % is the strategy's pick (an unproven tier above the cap is recorded as clamped
|
|
367
|
+
// from its configured size); every ceiling below still applies.
|
|
368
|
+
if (tier) {
|
|
369
|
+
usd = (bank * tier.effectivePct) / 100;
|
|
370
|
+
basis = `tier ${tier.tier} (score ${tier.score}/${tier.of}) ${tier.effectivePct}% of $${r2(bank)}`;
|
|
371
|
+
if (tier.capped)
|
|
372
|
+
clamps.push({ rule: 'tier-unproven-cap', fromUsd: r2((bank * tier.configuredPct) / 100), toUsd: r2(usd) });
|
|
369
373
|
}
|
|
374
|
+
else
|
|
375
|
+
switch (z.mode) {
|
|
376
|
+
case 'fixed':
|
|
377
|
+
usd = z.fixedUsd ?? 0;
|
|
378
|
+
basis = `fixed $${z.fixedUsd}`;
|
|
379
|
+
break;
|
|
380
|
+
case 'pct':
|
|
381
|
+
usd = (bank * (z.pct ?? 0)) / 100;
|
|
382
|
+
basis = `${z.pct}% of $${r2(bank)}`;
|
|
383
|
+
break;
|
|
384
|
+
case 'risk': {
|
|
385
|
+
const stop = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
|
|
386
|
+
usd = (bank * (z.riskPct ?? 0)) / stop;
|
|
387
|
+
basis = `${z.riskPct}% risk / ${stop}% stop of $${r2(bank)}`;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case 'confidence': {
|
|
391
|
+
const c = z.confidence ?? { minPct: 1, maxPct: 1, default: 0.5 };
|
|
392
|
+
const conf = clamp01(req.confidence ?? c.default);
|
|
393
|
+
const pct = c.minPct + (c.maxPct - c.minPct) * conf;
|
|
394
|
+
usd = (bank * pct) / 100;
|
|
395
|
+
basis = `confidence ${r2(conf)} → ${r2(pct)}% of $${r2(bank)}`;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
370
399
|
// The agent may ask for LESS than the strategy's pick (never more).
|
|
371
400
|
const asked = req.sizeUsd ?? (req.sizePct !== undefined ? (bank * req.sizePct) / 100 : undefined);
|
|
372
401
|
if (asked !== undefined && Number.isFinite(asked)) {
|
|
@@ -381,6 +410,12 @@ export function clampEntry(book, k, req, killSwitch = false) {
|
|
|
381
410
|
clamps.push({ rule, fromUsd: r2(usd), toUsd: r2(Math.max(0, limit)) });
|
|
382
411
|
usd = Math.max(0, limit);
|
|
383
412
|
} };
|
|
413
|
+
// The engine's own ceiling for a tiered entry, whatever the tier pick said: code, not the table or the strategy file.
|
|
414
|
+
if (tier) {
|
|
415
|
+
if (!Number.isFinite(usd))
|
|
416
|
+
usd = 0;
|
|
417
|
+
cap('tier-hard-cap', tierHardCapUsd(bank));
|
|
418
|
+
}
|
|
384
419
|
if (st.sizeDown)
|
|
385
420
|
cap('drawdown-size-down', usd * s.risk.drawdown.sizeDownMult);
|
|
386
421
|
cap('strategy-max-pct', (bank * s.sizing.maxPctOfBankroll) / 100);
|
|
@@ -388,14 +423,25 @@ export function clampEntry(book, k, req, killSwitch = false) {
|
|
|
388
423
|
cap('strategy-max-usd', s.sizing.maxUsd);
|
|
389
424
|
const lossPct = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
|
|
390
425
|
cap('per-trade-max-loss', (bank * s.risk.perTrade.maxLossPctOfBankroll) / lossPct);
|
|
391
|
-
cap('max-exposure', (bank * s.risk.maxExposurePct) / 100 - book.exposureUsd());
|
|
392
|
-
cap('cash', st.cashUsd);
|
|
426
|
+
cap('max-exposure', (bank * s.risk.maxExposurePct) / 100 - book.exposureUsd() - fly.usd);
|
|
427
|
+
cap('cash', st.cashUsd - fly.usd);
|
|
393
428
|
const R = (k.liq ?? 0) / 2;
|
|
394
429
|
const imp = s.execution.maxPriceImpactPct / 100;
|
|
395
430
|
cap('price-impact', (R * imp) / (1 - s.execution.feeBps / 1e4) + s.execution.networkFeeUsd);
|
|
396
|
-
if (usd < s.sizing.minUsd)
|
|
397
|
-
|
|
431
|
+
if (usd < s.sizing.minUsd) {
|
|
432
|
+
const r = no(clamps.length ? `below-min-after-${clamps[clamps.length - 1].rule}` : 'below-min-size');
|
|
433
|
+
return tier ? { ...r, tier } : r;
|
|
434
|
+
}
|
|
398
435
|
const first = z.ladder ? usd * z.ladder[0].frac : usd;
|
|
399
|
-
return { ok: true, usd: r2(usd), firstTrancheUsd: r2(first), profile, clamps, basis };
|
|
436
|
+
return { ok: true, usd: r2(usd), firstTrancheUsd: r2(first), profile, clamps, basis, ...(tier ? { tier } : {}) };
|
|
437
|
+
}
|
|
438
|
+
/** In-flight entries, failing closed: a negative or non-finite count is one open entry, such dollars are the whole room. */
|
|
439
|
+
const inFlightOf = (f) => ({
|
|
440
|
+
opens: f === undefined ? 0 : Number.isFinite(f.opens) && f.opens >= 0 ? f.opens : 1,
|
|
441
|
+
usd: f === undefined ? 0 : Number.isFinite(f.usd) && f.usd >= 0 ? f.usd : Infinity,
|
|
442
|
+
});
|
|
443
|
+
/** The most a tiered position may cost in total (every tranche and add): TIERS_HARD_CAP_PCT of the book. */
|
|
444
|
+
export function tierHardCapUsd(bookUsd) {
|
|
445
|
+
return Number.isFinite(bookUsd) && bookUsd > 0 ? (bookUsd * TIERS_HARD_CAP_PCT) / 100 : 0;
|
|
400
446
|
}
|
|
401
447
|
const clamp01 = (x) => (Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : 0.5);
|
|
@@ -268,7 +268,7 @@ export class MemberGate extends SessionGate {
|
|
|
268
268
|
overDay() { const o = super.overDay(); if (o)
|
|
269
269
|
return o; const g = this.fleet.gate.overDay(); return g ? `fleet-${g}` : null; }
|
|
270
270
|
rateLimit(r) { super.rateLimit(r); this.fleet.rateLimit(r, this); }
|
|
271
|
-
block(reason) { super.block(reason); this.fleet.block(reason, this); }
|
|
271
|
+
block(reason, sessionId) { super.block(reason, sessionId); this.fleet.block(reason, this); }
|
|
272
272
|
paused() { return super.paused() || this.fleet.paused(); }
|
|
273
273
|
/** A reading another strategy's session made (the same Claude account). */
|
|
274
274
|
shareRateLimit(r) { super.rateLimit(r); }
|
|
@@ -543,7 +543,7 @@ export async function runFleet(o) {
|
|
|
543
543
|
l.done = runAutotrade({
|
|
544
544
|
...(adopt?.length ? { adopt } : {}),
|
|
545
545
|
strategy: spec.strategy, source: { name: o.tape?.source ?? o.source.name, ticks, startT: now }, sessions, gate, ledger, runId,
|
|
546
|
-
dossier: o.dossier?.(spec), vet: o.vet?.(spec), executor: execs.executors.get(id), verbose: o.verbose ?? true, onLine: (s) => say(`[${id}] ${s}`),
|
|
546
|
+
dossier: o.dossier?.(spec), vet: o.vet?.(spec), drain: o.drain?.(spec), executor: execs.executors.get(id), verbose: o.verbose ?? true, onLine: (s) => say(`[${id}] ${s}`),
|
|
547
547
|
pacing, ...(account ? { account, member: id } : {}),
|
|
548
548
|
safety: () => {
|
|
549
549
|
const g = o.globalSafety();
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// execution-port where an approved entry goes: the paper port (local single-use proofs) and the live stub (task-1527)
|
|
23
23
|
// chat Telegram notify, control and approve-to-trade through the user's own bot; chat/ (task-1527)
|
|
24
24
|
// yolo task-1532: the full-port replay (rugs, loss spread, how often it zeroed the wallet); paper only
|
|
25
|
+
// tiers 0.2.2: EXPERIMENTAL, UNPROVEN confidence-tiered sizing (sizing.tiers), PAPER ONLY, default off
|
|
25
26
|
// signal / outbox / signal-channels / receipts / real-mode task-1645: mode real = the paper engine + a LIVE TRADE FIRE
|
|
26
27
|
// signal per fill to the user's own executor (webhook, command hook, JSONL, MCP feed, Telegram copy); no key
|
|
27
28
|
// limits / executor-health / report task-1654: every real-mode limit user-set per scope (stricter wins), executor
|
|
@@ -55,6 +56,9 @@ export * from "./trade-ledger.js";
|
|
|
55
56
|
export * from "./execution-port.js";
|
|
56
57
|
export * from "./chat/index.js";
|
|
57
58
|
export * from "./yolo.js";
|
|
59
|
+
export * from "./rugproof.js";
|
|
60
|
+
export * from "./rugproof-probe.js";
|
|
61
|
+
export * from "./tiers.js";
|
|
58
62
|
export * from "./signal.js";
|
|
59
63
|
export * from "./outbox.js";
|
|
60
64
|
export * from "./signal-channels.js";
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// dexterm autotrade — the rug-proof probes (task-1748): the network half of rugproof.ts. READ-ONLY, PAPER ONLY.
|
|
2
|
+
//
|
|
3
|
+
// DexScreener tokens/v1 the coin's deepest pool: dex, base / quote mint, creation time, liquidity keyless
|
|
4
|
+
// Solana RPC the PumpSwap pool account (creator, lp mint, vaults), the LP mint's supply and
|
|
5
|
+
// largest holders, the pool's newest transactions (Withdraw scan), the SOL vault public or the user's own key
|
|
6
|
+
// Jupiter lite quote token -> SOL for a given raw amount keyless
|
|
7
|
+
//
|
|
8
|
+
// No wallet, no key of any wallet, no signing, no send: only reads. An RPC URL may carry an api key: it is never
|
|
9
|
+
// printed (redactSecrets) and never written to the ledger. Every read has a timeout; a read that fails is `error`
|
|
10
|
+
// and the gate that needed it refuses (fail closed). The vet runs the gates in rugproof.ts order and stops at the first
|
|
11
|
+
// refusal, so a coin refused by a free gate costs no network call.
|
|
12
|
+
import { redactSecrets, base58 } from '../../data/src/index.js';
|
|
13
|
+
import { PUMPSWAP_PROGRAM, WSOL_MINT, drainVerdict, gateHolders, gateLiquidityRecheck, gateLpLocked, gatePoolAge, gatePoolStructure, gateSellRoute, gateWithdrawHistory, verdictOf, } from "./rugproof.js";
|
|
14
|
+
export const defaultFetchJson = (timeoutMs = 12_000) => async (url, init) => {
|
|
15
|
+
const r = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
16
|
+
let json = null;
|
|
17
|
+
try {
|
|
18
|
+
json = await r.json();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
json = null;
|
|
22
|
+
}
|
|
23
|
+
return { status: r.status, json };
|
|
24
|
+
};
|
|
25
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
|
+
const isObj = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
|
|
27
|
+
const str = (x) => (typeof x === 'string' && x ? x : null);
|
|
28
|
+
const fin = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
29
|
+
export class RugProbe {
|
|
30
|
+
id = 0;
|
|
31
|
+
d;
|
|
32
|
+
/** Per-run cache: a pool's structure does not change, its LP and history are re-read at most every 10 min. */
|
|
33
|
+
lpCache = new Map();
|
|
34
|
+
decimals = new Map();
|
|
35
|
+
rpcCalls = 0;
|
|
36
|
+
/** Backoff unit after a rate limit (ms); tests set 0. */
|
|
37
|
+
backoffMs;
|
|
38
|
+
constructor(d) { this.d = d; this.backoffMs = d.backoffMs ?? 1500; }
|
|
39
|
+
async rpc(method, params) {
|
|
40
|
+
if (!this.d.rpcUrl)
|
|
41
|
+
throw new Error('no RPC endpoint');
|
|
42
|
+
let r = { status: 0, json: null };
|
|
43
|
+
// A rate limit (HTTP 429 or JSON-RPC -32429) is retried twice with a backoff before the read counts as failed.
|
|
44
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
45
|
+
this.rpcCalls++;
|
|
46
|
+
try {
|
|
47
|
+
r = await this.d.fetchJson(this.d.rpcUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++this.id, method, params }) });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new Error(redactSecrets(`rpc ${method}: ${e.message}`).slice(0, 200));
|
|
51
|
+
}
|
|
52
|
+
const limited = r.status === 429 || (isObj(r.json) && isObj(r.json.error) && r.json.error.code === -32429);
|
|
53
|
+
if (!limited || attempt === 2)
|
|
54
|
+
break;
|
|
55
|
+
await sleep(this.backoffMs * (attempt + 1));
|
|
56
|
+
}
|
|
57
|
+
if (!isObj(r.json))
|
|
58
|
+
throw new Error(`rpc ${method}: HTTP ${r.status}`);
|
|
59
|
+
if (r.json.error)
|
|
60
|
+
throw new Error(redactSecrets(`rpc ${method}: ${JSON.stringify(r.json.error)}`).slice(0, 200));
|
|
61
|
+
return r.json.result;
|
|
62
|
+
}
|
|
63
|
+
pairCache = new Map();
|
|
64
|
+
scanCache = new Map();
|
|
65
|
+
/** The coin's deepest DexScreener pool (shared for 30 s: every fleet member asks about the same coin). */
|
|
66
|
+
pair(mint, now = Date.now()) {
|
|
67
|
+
const hit = this.pairCache.get(mint);
|
|
68
|
+
if (hit && now - hit.t < 30_000)
|
|
69
|
+
return hit.v;
|
|
70
|
+
const v = this.readPair(mint);
|
|
71
|
+
this.pairCache.set(mint, { t: now, v });
|
|
72
|
+
v.catch(() => this.pairCache.delete(mint));
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
async readPair(mint) {
|
|
76
|
+
const r = await this.d.fetchJson(`${this.d.dexscreenerBase ?? 'https://api.dexscreener.com'}/tokens/v1/solana/${mint}`);
|
|
77
|
+
if (!Array.isArray(r.json))
|
|
78
|
+
throw new Error(`dexscreener HTTP ${r.status}`);
|
|
79
|
+
const pairs = r.json.filter((p) => isObj(p) && str(p.pairAddress)).sort((a, b) => (fin(b.liquidity?.usd) ?? 0) - (fin(a.liquidity?.usd) ?? 0));
|
|
80
|
+
const p = pairs[0];
|
|
81
|
+
if (!p)
|
|
82
|
+
return null;
|
|
83
|
+
return { address: p.pairAddress, dex: str(p.dexId) ?? 'unknown', baseMint: str(p.baseToken?.address) ?? '', quoteMint: str(p.quoteToken?.address) ?? '', createdAt: fin(p.pairCreatedAt), liqUsd: fin(p.liquidity?.usd) };
|
|
84
|
+
}
|
|
85
|
+
/** The PumpSwap pool account + its LP mint (supply, largest holders' owners). null = not a PumpSwap account. */
|
|
86
|
+
async poolLp(pool, now = Date.now()) {
|
|
87
|
+
const hit = this.lpCache.get(pool);
|
|
88
|
+
if (hit && now - hit.t < 10 * 60e3 && hit.v !== 'error')
|
|
89
|
+
return hit.v;
|
|
90
|
+
let v;
|
|
91
|
+
try {
|
|
92
|
+
v = await this.readPoolLp(pool);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
v = 'error';
|
|
96
|
+
}
|
|
97
|
+
this.lpCache.set(pool, { t: now, v });
|
|
98
|
+
return v;
|
|
99
|
+
}
|
|
100
|
+
async readPoolLp(pool) {
|
|
101
|
+
const acc = await this.rpc('getAccountInfo', [pool, { encoding: 'base64' }]);
|
|
102
|
+
const value = isObj(acc) ? acc.value : null;
|
|
103
|
+
if (!value)
|
|
104
|
+
return null;
|
|
105
|
+
if (value.owner !== PUMPSWAP_PROGRAM)
|
|
106
|
+
return null;
|
|
107
|
+
const data = Buffer.from(String(value.data[0] ?? ''), 'base64');
|
|
108
|
+
const lp = decodePumpSwapPool(data);
|
|
109
|
+
const sup = await this.rpc('getTokenSupply', [lp.lpMint]);
|
|
110
|
+
const lpSupply = BigInt(String((sup?.value?.amount) ?? '0'));
|
|
111
|
+
let holders = [];
|
|
112
|
+
if (lpSupply > 0n) {
|
|
113
|
+
const big = await this.rpc('getTokenLargestAccounts', [lp.lpMint]);
|
|
114
|
+
const rows = ((big?.value ?? []).filter(isObj)).slice(0, 5);
|
|
115
|
+
const accs = rows.length ? await this.rpc('getMultipleAccounts', [rows.map((x) => x.address), { encoding: 'jsonParsed' }]) : null;
|
|
116
|
+
const vals = accs?.value ?? [];
|
|
117
|
+
holders = rows.map((x, i) => {
|
|
118
|
+
const owner = str(vals[i]?.data?.parsed?.info?.owner) ?? 'unknown';
|
|
119
|
+
return { owner, amount: BigInt(String(x.amount ?? '0')) };
|
|
120
|
+
}).sort((a, b) => (b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0));
|
|
121
|
+
}
|
|
122
|
+
return { ...lp, lpSupply, holders };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `Withdraw` instructions among the pool's newest `n` successful transactions (and how many the creator signed).
|
|
126
|
+
* Fails closed: one transaction that cannot be read (after the rpc's rate-limit retries) could be the creator's
|
|
127
|
+
* Withdraw, so any unread one makes the whole scan `error` (the gate refuses, the vet looks again in 5 min).
|
|
128
|
+
*/
|
|
129
|
+
async withdraws(pool, creator, n, now = Date.now()) {
|
|
130
|
+
const hit = this.scanCache.get(pool);
|
|
131
|
+
if (hit && now - hit.t < 10 * 60e3)
|
|
132
|
+
return hit.v;
|
|
133
|
+
const v = await this.scanWithdraws(pool, creator, n);
|
|
134
|
+
if (v !== 'error')
|
|
135
|
+
this.scanCache.set(pool, { t: now, v });
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
async scanWithdraws(pool, creator, n) {
|
|
139
|
+
try {
|
|
140
|
+
const sigs = await this.rpc('getSignaturesForAddress', [pool, { limit: Math.min(100, n * 2) }]);
|
|
141
|
+
const ok = (Array.isArray(sigs) ? sigs : []).filter((x) => isObj(x) && !x.err).slice(0, n);
|
|
142
|
+
let withdraws = 0;
|
|
143
|
+
let byCreator = 0;
|
|
144
|
+
let scanned = 0;
|
|
145
|
+
for (const s of ok) {
|
|
146
|
+
// maxSupportedTransactionVersion 1: Helius answers -32015 for a version-1 transaction under 0 (task-1394).
|
|
147
|
+
let tx = null;
|
|
148
|
+
try {
|
|
149
|
+
tx = await this.rpc('getTransaction', [s.signature, { encoding: 'jsonParsed', maxSupportedTransactionVersion: 1 }]);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return 'error';
|
|
153
|
+
}
|
|
154
|
+
if (!isObj(tx))
|
|
155
|
+
return 'error';
|
|
156
|
+
scanned++;
|
|
157
|
+
const logs = tx.meta?.logMessages ?? [];
|
|
158
|
+
if (!logs.some((l) => l === 'Program log: Instruction: Withdraw'))
|
|
159
|
+
continue;
|
|
160
|
+
withdraws++;
|
|
161
|
+
const keys = tx.transaction?.message?.accountKeys ?? [];
|
|
162
|
+
const signer = keys.map((k) => (isObj(k) ? k.pubkey : k))[0];
|
|
163
|
+
if (signer === creator)
|
|
164
|
+
byCreator++;
|
|
165
|
+
}
|
|
166
|
+
return { scanned, withdraws, byCreator };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return 'error';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async tokenDecimals(mint) {
|
|
173
|
+
const hit = this.decimals.get(mint);
|
|
174
|
+
if (hit !== undefined)
|
|
175
|
+
return hit;
|
|
176
|
+
try {
|
|
177
|
+
const sup = await this.rpc('getTokenSupply', [mint]);
|
|
178
|
+
const d = fin(sup?.value?.decimals);
|
|
179
|
+
if (d !== null) {
|
|
180
|
+
this.decimals.set(mint, d);
|
|
181
|
+
return d;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* below */ }
|
|
185
|
+
// pump.fun mints are 6 decimals by construction.
|
|
186
|
+
return mint.endsWith('pump') ? 6 : null;
|
|
187
|
+
}
|
|
188
|
+
/** Jupiter token -> SOL quote for `raw` units. HTTP 429 / a network failure is not a quote error. */
|
|
189
|
+
async route(mint, raw) {
|
|
190
|
+
if (raw <= 0n)
|
|
191
|
+
return { ok: false, kind: 'no-route', error: 'zero amount' };
|
|
192
|
+
let r = { status: 0, json: null };
|
|
193
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
194
|
+
try {
|
|
195
|
+
r = await this.d.fetchJson(`${this.d.jupiterBase ?? 'https://lite-api.jup.ag'}/swap/v1/quote?inputMint=${mint}&outputMint=${WSOL_MINT}&amount=${raw}&slippageBps=1000`);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
return { ok: false, kind: 'network', error: e.message.slice(0, 120) };
|
|
199
|
+
}
|
|
200
|
+
if (r.status !== 429 || attempt === 2)
|
|
201
|
+
break;
|
|
202
|
+
await sleep(this.backoffMs * (attempt + 1));
|
|
203
|
+
}
|
|
204
|
+
if (r.status === 429)
|
|
205
|
+
return { ok: false, kind: 'rate-limited', error: 'HTTP 429' };
|
|
206
|
+
const j = isObj(r.json) ? r.json : {};
|
|
207
|
+
if (str(j.outAmount) && !j.errorCode && !j.error)
|
|
208
|
+
return { ok: true, outLamports: BigInt(j.outAmount), impactPct: fin(Number(j.priceImpactPct)) === null ? null : Math.round(Number(j.priceImpactPct) * 10000) / 100 };
|
|
209
|
+
if (r.status >= 500 && !j.errorCode)
|
|
210
|
+
return { ok: false, kind: 'network', error: `HTTP ${r.status}` };
|
|
211
|
+
return { ok: false, kind: 'no-route', error: String(j.errorCode ?? j.error ?? `HTTP ${r.status}`).slice(0, 120) };
|
|
212
|
+
}
|
|
213
|
+
/** The pool's SOL vault balance (SOL). */
|
|
214
|
+
async vaultSol(vault) {
|
|
215
|
+
try {
|
|
216
|
+
const b = await this.rpc('getTokenAccountBalance', [vault]);
|
|
217
|
+
const v = b?.value ?? {};
|
|
218
|
+
return fin(v.uiAmount) ?? (str(v.amount) ? Number(v.amount) / 1e9 : null);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** PumpSwap `Pool`: disc 8, bump u8, index u16, creator, base_mint, quote_mint, lp_mint, base vault, quote vault, lp_supply u64, coin_creator. */
|
|
226
|
+
export function decodePumpSwapPool(d) {
|
|
227
|
+
if (d.length < 211)
|
|
228
|
+
throw new Error(`PumpSwap pool account too short (${d.length} bytes)`);
|
|
229
|
+
const key = (o) => base58(d.subarray(o, o + 32));
|
|
230
|
+
return { creator: key(11), baseMint: key(43), quoteMint: key(75), lpMint: key(107), baseVault: key(139), quoteVault: key(171) };
|
|
231
|
+
}
|
|
232
|
+
// ───────────────────────────── the vet ─────────────────────────────
|
|
233
|
+
export const UNKNOWN_RETRY_MS = 5 * 60e3;
|
|
234
|
+
/**
|
|
235
|
+
* The strategy's rug-proof gates as a runner vet: free gates first (tick + history), then the pair, the pool account,
|
|
236
|
+
* the withdraw scan and the Jupiter route. The first refusal ends it; a time gate asks to be retried.
|
|
237
|
+
*/
|
|
238
|
+
export function rugproofVet(r, o) {
|
|
239
|
+
return async (k, ctx) => {
|
|
240
|
+
const results = [];
|
|
241
|
+
const now = k.t;
|
|
242
|
+
const done = (x) => {
|
|
243
|
+
results.push(x);
|
|
244
|
+
if (x.pass)
|
|
245
|
+
return null;
|
|
246
|
+
o.log?.({ mint: k.mint, symbol: k.symbol, gate: x.gate, rule: x.rule, detail: x.detail, results: results.map((y) => `${y.gate}:${y.pass ? 'ok' : 'no'}`) });
|
|
247
|
+
// A read that failed (`-unknown`: a rate limit, an RPC outage) refuses now (fail closed) and looks again in 5 min,
|
|
248
|
+
// instead of skipping the coin for the rest of the run; a structural refusal is for good.
|
|
249
|
+
const retry = x.retryAfterMs ?? (x.rule.endsWith('-unknown') ? UNKNOWN_RETRY_MS : undefined);
|
|
250
|
+
return { ok: false, rule: x.rule, detail: x.detail, ...(retry !== undefined ? { retryAfterMs: retry } : {}) };
|
|
251
|
+
};
|
|
252
|
+
const history = ctx?.history ?? [];
|
|
253
|
+
const sizeUsd = ctx?.sizeUsd ?? 0;
|
|
254
|
+
const floor = ctx?.exitFloorUsd ?? 2000;
|
|
255
|
+
// 1-3: free.
|
|
256
|
+
let f = done(gatePoolAge(r, k, undefined, now));
|
|
257
|
+
if (f)
|
|
258
|
+
return f;
|
|
259
|
+
f = done(gateHolders(k, ctx?.maxTop10Pct ?? null, ctx?.maxDevPct ?? null, ctx?.unknownHolders));
|
|
260
|
+
if (f)
|
|
261
|
+
return f;
|
|
262
|
+
f = done(gateLiquidityRecheck(r, history, k, floor));
|
|
263
|
+
if (f)
|
|
264
|
+
return f;
|
|
265
|
+
// 4: the pool as listed.
|
|
266
|
+
let pair = null;
|
|
267
|
+
let pairErr = null;
|
|
268
|
+
try {
|
|
269
|
+
pair = await o.probe.pair(k.mint);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
pairErr = e.message.slice(0, 160);
|
|
273
|
+
}
|
|
274
|
+
results.shift(); // the token-age pass above is replaced by the pool's own age
|
|
275
|
+
if (pairErr !== null)
|
|
276
|
+
return done({ gate: 'pool-structure', pass: false, rule: 'rugproof:pool-structure-unknown', detail: `the pool could not be read: ${pairErr}` });
|
|
277
|
+
f = done(gatePoolAge(r, k, pair ? pair.createdAt : null, now));
|
|
278
|
+
if (f)
|
|
279
|
+
return f;
|
|
280
|
+
f = done(gatePoolStructure(r, k.mint, k.launchpad, pair));
|
|
281
|
+
if (f)
|
|
282
|
+
return f;
|
|
283
|
+
// 5-6: the chain.
|
|
284
|
+
let lp = null;
|
|
285
|
+
if (r.requireSafePool && pair) {
|
|
286
|
+
lp = o.probe.d.rpcUrl ? await o.probe.poolLp(pair.address, now) : 'error';
|
|
287
|
+
f = done(gateLpLocked(r, pair, lp, k.mint));
|
|
288
|
+
if (f)
|
|
289
|
+
return f;
|
|
290
|
+
const poolLp = lp;
|
|
291
|
+
const scan = poolLp.lpSupply === 0n && r.maxWithdraws === 0 ? null : await o.probe.withdraws(pair.address, poolLp.creator, r.withdrawScanTx);
|
|
292
|
+
f = done(gateWithdrawHistory(r, scan, poolLp));
|
|
293
|
+
if (f)
|
|
294
|
+
return f;
|
|
295
|
+
}
|
|
296
|
+
// 7: a sell route at routeX x the size.
|
|
297
|
+
if (r.routeX !== null) {
|
|
298
|
+
const dec = await o.probe.tokenDecimals(k.mint);
|
|
299
|
+
let route = null;
|
|
300
|
+
if (dec !== null && k.price) {
|
|
301
|
+
const tokens = (sizeUsd * r.routeX) / k.price;
|
|
302
|
+
route = await o.probe.route(k.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** dec))));
|
|
303
|
+
}
|
|
304
|
+
f = done(gateSellRoute(r, route, sizeUsd));
|
|
305
|
+
if (f)
|
|
306
|
+
return f;
|
|
307
|
+
}
|
|
308
|
+
const v = verdictOf(r, results, pair?.createdAt ? now - pair.createdAt : null);
|
|
309
|
+
const note = `rugproof ok: ${results.map((x) => `${x.gate}${x.partial ? '~' : ''}`).join(', ')}${v.sizeCapPct !== null ? `; size cap ${v.sizeCapPct}% (young or partly verified pool)` : ''}`;
|
|
310
|
+
o.log?.({ mint: k.mint, symbol: k.symbol, pass: true, sizeCapPct: v.sizeCapPct, results: results.map((x) => `${x.gate}:${x.partial ? 'partial' : 'ok'}`) });
|
|
311
|
+
return { ok: true, note, recheck: true, ...(v.sizeCapPct !== null ? { sizeCapPct: v.sizeCapPct } : {}), ...(pair ? { pool: { address: pair.address, quoteVault: lp && lp !== 'error' ? lp.quoteVault : null } } : {}) };
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Polls every open position's pool (SOL vault when the pool is known, plus a Jupiter quote for the held amount)
|
|
316
|
+
* every pollSec, outside the candle / tick cadence. The runner reads `verdict()` on every step and sells the whole
|
|
317
|
+
* position on an exit verdict. Timers are unref'd: they never keep a process alive.
|
|
318
|
+
*/
|
|
319
|
+
export class DrainPoller {
|
|
320
|
+
w = new Map();
|
|
321
|
+
g;
|
|
322
|
+
probe;
|
|
323
|
+
log;
|
|
324
|
+
constructor(g, probe, log) { this.g = g; this.probe = probe; this.log = log; }
|
|
325
|
+
watch(p) {
|
|
326
|
+
if (this.w.has(p.positionId))
|
|
327
|
+
return;
|
|
328
|
+
const x = { pos: p, base: { reserveSol: null, liqUsd: p.entryLiqUsd }, timer: null, verdict: null, reads: 0, quoteErrors: 0, noRoute: 0, busy: false, decimals: null };
|
|
329
|
+
this.w.set(p.positionId, x);
|
|
330
|
+
void this.poll(x, true);
|
|
331
|
+
x.timer = setInterval(() => { void this.poll(x, false); }, Math.max(2, Math.min(10, this.g.pollSec)) * 1000);
|
|
332
|
+
x.timer.unref?.();
|
|
333
|
+
}
|
|
334
|
+
unwatch(positionId) {
|
|
335
|
+
const x = this.w.get(positionId);
|
|
336
|
+
if (x?.timer)
|
|
337
|
+
clearInterval(x.timer);
|
|
338
|
+
this.w.delete(positionId);
|
|
339
|
+
}
|
|
340
|
+
verdict(positionId) {
|
|
341
|
+
const v = this.w.get(positionId)?.verdict ?? null;
|
|
342
|
+
return v && v.exit ? v : null;
|
|
343
|
+
}
|
|
344
|
+
stop() { for (const id of [...this.w.keys()])
|
|
345
|
+
this.unwatch(id); }
|
|
346
|
+
/** One probe of one position (also called by tests). */
|
|
347
|
+
async poll(x, first) {
|
|
348
|
+
if (x.busy)
|
|
349
|
+
return;
|
|
350
|
+
x.busy = true;
|
|
351
|
+
try {
|
|
352
|
+
let vault = x.pos.quoteVault ?? null;
|
|
353
|
+
if (!vault && x.pos.poolAddress && this.probe.d.rpcUrl) {
|
|
354
|
+
const lp = await this.probe.poolLp(x.pos.poolAddress);
|
|
355
|
+
if (lp && lp !== 'error')
|
|
356
|
+
vault = x.pos.quoteVault = lp.quoteVault;
|
|
357
|
+
}
|
|
358
|
+
const reserveSol = vault && this.probe.d.rpcUrl ? await this.probe.vaultSol(vault) : null;
|
|
359
|
+
if (first || x.base.reserveSol === null) {
|
|
360
|
+
if (reserveSol !== null)
|
|
361
|
+
x.base.reserveSol = reserveSol;
|
|
362
|
+
}
|
|
363
|
+
x.decimals ??= await this.probe.tokenDecimals(x.pos.mint);
|
|
364
|
+
const tokens = x.pos.tokens();
|
|
365
|
+
const route = this.g.exitOnNoRoute && x.decimals !== null && tokens > 0 ? await this.probe.route(x.pos.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** x.decimals)))) : null;
|
|
366
|
+
x.reads++;
|
|
367
|
+
if (route && !route.ok && route.kind !== 'no-route')
|
|
368
|
+
x.quoteErrors++;
|
|
369
|
+
let v = drainVerdict(this.g, x.base, { t: Date.now(), reserveSol, liqUsd: null, route });
|
|
370
|
+
// A quote error alone needs two reads in a row (one pollSec apart): a single odd answer is not a pull. A reserve
|
|
371
|
+
// read decides at once. The poller keeps polling after an exit: a route that comes back lifts the verdict.
|
|
372
|
+
x.noRoute = v.exit && v.reason === 'drain-no-route' ? x.noRoute + 1 : 0;
|
|
373
|
+
if (v.exit && v.reason === 'drain-no-route' && x.noRoute < 2)
|
|
374
|
+
v = { exit: false, note: 'no route once: re-checking' };
|
|
375
|
+
const was = x.verdict?.exit ?? false;
|
|
376
|
+
x.verdict = v;
|
|
377
|
+
if (v.exit && !was)
|
|
378
|
+
this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, drain: v.reason, detail: v.detail, reads: x.reads });
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, probeError: e.message.slice(0, 160) });
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
x.busy = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
stats() {
|
|
388
|
+
let reads = 0;
|
|
389
|
+
let quoteErrors = 0;
|
|
390
|
+
for (const x of this.w.values()) {
|
|
391
|
+
reads += x.reads;
|
|
392
|
+
quoteErrors += x.quoteErrors;
|
|
393
|
+
}
|
|
394
|
+
return { watched: this.w.size, reads, quoteErrors };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** Run vets in order (the rug-proof gates first: they are the cheapest); the first refusal wins; notes and caps merge. */
|
|
398
|
+
export function chainVets(...vets) {
|
|
399
|
+
const vs = vets.filter((v) => !!v);
|
|
400
|
+
if (vs.length <= 1)
|
|
401
|
+
return vs[0];
|
|
402
|
+
return async (k, ctx) => {
|
|
403
|
+
const notes = [];
|
|
404
|
+
let out = { ok: true };
|
|
405
|
+
for (const v of vs) {
|
|
406
|
+
const r = await v(k, ctx);
|
|
407
|
+
if (!r.ok)
|
|
408
|
+
return r;
|
|
409
|
+
if (r.note)
|
|
410
|
+
notes.push(r.note);
|
|
411
|
+
out = { ...out, ...r, recheck: !!(out.ok && out.recheck) || !!r.recheck, ...(out.ok && out.sizeCapPct !== undefined && (r.sizeCapPct === undefined || out.sizeCapPct < r.sizeCapPct) ? { sizeCapPct: out.sizeCapPct } : {}) };
|
|
412
|
+
}
|
|
413
|
+
return notes.length ? { ...out, note: notes.join('; ') } : out;
|
|
414
|
+
};
|
|
415
|
+
}
|