-
-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathsniper-stall.js
More file actions
101 lines (90 loc) · 6.11 KB
/
Copy pathsniper-stall.js
File metadata and controls
101 lines (90 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Why an armed sniper strategy is not trading.
*
* An arm can be `enabled = true`, hold SOL, evaluate every launch that crosses
* the feed, and still never buy — because one of its own knobs makes an entry
* arithmetically impossible. That failure is silent by construction: a skipped
* evaluation writes no position row, so the board shows "0 trades" with no
* reason, and an arm can sit dead for a week without anything looking broken.
* Four arms did exactly that.
*
* This module names the condition from data the scoreboard already has. Every
* diagnosis is a fact about the configuration, not a guess: each one is a rule
* the executor really enforces, evaluated against the same numbers.
*
* Pure — no DB, no RPC, no clock. The caller supplies the facts.
*/
import { MIN_OPERATIONAL_WALLET_SOL } from './agent-trade-guards.js';
// A pump.fun launch is created ON the bonding curve at a fixed starting market
// cap — around $2.1k at current SOL prices, and never more than a few thousand
// dollars before its first buy. A `new_mint` trigger fires at exactly that
// moment, so a floor above this band can never be cleared by that trigger: by
// the time the coin is worth $10k the create event is long gone. Triggers that
// fire on an already-traded coin (intel_confirmed, oracle_crossing,
// graduation_ride) are the ones that can satisfy a band.
export const LAUNCH_MCAP_USD = 5_000;
// What a launch is actually worth at the create event. Sampled live: 40 of 40
// fresh pump.fun mints priced between $0.1k and $5.8k, median $2.1k. A floor
// between this and LAUNCH_MCAP_USD is reachable but rare (about 5% of launches),
// which is worth saying out loud without calling the arm broken.
export const TYPICAL_LAUNCH_MCAP_USD = 2_500;
const LAUNCH_TRIGGERS = new Set(['new_mint']);
// Floor a wallet must hold to open anything: entry overhead (ATA rent, fee and
// tip headroom) plus the firewall's round-trip probe reserve plus a minimum
// entry. Under this the arm cannot even prove the coin is sellable, so it never
// broadcasts. Imported, not redefined — the reclaim that moves SOL out of these
// wallets reads the same number, and the two disagreeing is what emptied them.
export const MIN_WALLET_SOL = MIN_OPERATIONAL_WALLET_SOL;
const num = (v) => (v == null || !Number.isFinite(Number(v)) ? null : Number(v));
/**
* @param {object} o
* @param {object} o.strategy agent_sniper_strategies row (snake_case)
* @param {number} [o.closed] real closes in the window
* @param {number} [o.open] open positions
* @param {number|null} [o.balanceSol] live wallet SOL, or null when unread
* @param {number} [o.verdictCount] LLM verdicts requested for this arm's model
* @param {number} [o.namedModelAnswers] of those, how many the NAMED model answered
* @returns {{ code:string, blocking:boolean, message:string, also:Array<{code:string,blocking:boolean,message:string}> }|null}
* The most severe condition found, with every other condition in `also`. An arm
* is often broken in more than one way at once (a dry wallet AND an unreachable
* band); reporting only the first means the second surfaces a day later, after
* someone fixed the first and waited.
*/
export function diagnoseStall({ strategy, closed = 0, open = 0, balanceSol = null, verdictCount = 0, namedModelAnswers = 0 } = {}) {
const s = strategy || {};
const found = [];
const add = (code, blocking, message) => { found.push({ code, blocking, message }); };
if (s.enabled !== true) add('disabled', true, 'This arm is switched off, so it evaluates nothing.');
if (s.kill_switch === true) add('kill_switch', true, 'The per-strategy kill switch is engaged, so no entry can broadcast.');
if (balanceSol != null && balanceSol < MIN_WALLET_SOL) {
add('wallet_dry', true,
`Wallet holds ${balanceSol.toFixed(4)} SOL, under the ~${MIN_WALLET_SOL} SOL needed to fund the safety simulation and a minimum entry, so no buy can be attempted.`);
}
const minMcap = num(s.min_market_cap_usd);
if (LAUNCH_TRIGGERS.has(s.trigger || 'new_mint') && minMcap != null && minMcap > LAUNCH_MCAP_USD) {
add('mcap_band_unreachable', true,
`The ${s.trigger || 'new_mint'} trigger fires at creation, when a launch is worth about $2k, so this arm's $${Math.round(minMcap).toLocaleString('en-US')} floor can never be cleared at that moment. An intel_confirmed or oracle_crossing trigger scores a coin after it has traded into the band.`);
} else if (LAUNCH_TRIGGERS.has(s.trigger || 'new_mint') && minMcap != null && minMcap > TYPICAL_LAUNCH_MCAP_USD && closed === 0) {
add('mcap_band_tight', false,
`A launch is worth about $2k at the moment ${s.trigger || 'new_mint'} fires, so this arm's $${Math.round(minMcap).toLocaleString('en-US')} floor only clears the small minority of launches that open unusually high.`);
}
if ((s.decision_mode || 'rules') === 'llm' && s.llm_strict_model === true && verdictCount > 0 && namedModelAnswers === 0) {
add('strict_model_offline', true,
`Strict-model arm: every one of its last ${verdictCount} verdicts came from the failover chain rather than ${s.llm_model || 'the named model'}, and a strict arm refuses to trade on a fallback's judgment. Restore the named model's route to resume.`);
}
const perTrade = num(s.per_trade_lamports);
const dailyBudget = num(s.daily_budget_lamports);
if (perTrade != null && dailyBudget != null && dailyBudget > 0 && perTrade > dailyBudget) {
add('size_over_budget', false,
`Configured size (${(perTrade / 1e9).toFixed(4)} SOL) is larger than the whole daily budget (${(dailyBudget / 1e9).toFixed(4)} SOL), so entries are clamped down to the day's remainder.`);
}
if (!found.length && closed === 0 && open === 0) {
add('no_qualifying_launch', false, 'Config is reachable; nothing has met this arm\'s entry conditions in the window yet.');
}
if (!found.length) return null;
// Blocking conditions outrank advisory ones; within a rank the first found wins,
// and the checks above are ordered by how directly each one stops a trade.
const ranked = [...found].sort((a, b) => Number(b.blocking) - Number(a.blocking));
const [primary, ...rest] = ranked;
return { ...primary, also: rest };
}