candle-launch-bot 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of candle-launch-bot might be problematic. Click here for more details.

Files changed (66) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +400 -2
  4. package/bin/candle-launch-bot +7 -0
  5. package/dist/alerts.d.ts +28 -0
  6. package/dist/alerts.js +82 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +321 -0
  9. package/dist/config.d.ts +88 -0
  10. package/dist/config.js +244 -0
  11. package/dist/detect/detector.d.ts +111 -0
  12. package/dist/detect/detector.js +348 -0
  13. package/dist/engine.d.ts +103 -0
  14. package/dist/engine.js +585 -0
  15. package/dist/exec/executor.d.ts +149 -0
  16. package/dist/exec/executor.js +859 -0
  17. package/dist/exec/fills.d.ts +25 -0
  18. package/dist/exec/fills.js +53 -0
  19. package/dist/exec/trader.d.ts +109 -0
  20. package/dist/exec/trader.js +310 -0
  21. package/dist/filters.d.ts +26 -0
  22. package/dist/filters.js +92 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +2 -0
  25. package/dist/init.d.ts +23 -0
  26. package/dist/init.js +192 -0
  27. package/dist/ledger.d.ts +189 -0
  28. package/dist/ledger.js +243 -0
  29. package/dist/log.d.ts +22 -0
  30. package/dist/log.js +40 -0
  31. package/dist/market/curve.d.ts +80 -0
  32. package/dist/market/curve.js +168 -0
  33. package/dist/market/reader.d.ts +37 -0
  34. package/dist/market/reader.js +106 -0
  35. package/dist/redact.d.ts +20 -0
  36. package/dist/redact.js +111 -0
  37. package/dist/solana/base58.d.ts +4 -0
  38. package/dist/solana/base58.js +45 -0
  39. package/dist/solana/logs-ws.d.ts +25 -0
  40. package/dist/solana/logs-ws.js +116 -0
  41. package/dist/solana/pda.d.ts +12 -0
  42. package/dist/solana/pda.js +70 -0
  43. package/dist/solana/rpc.d.ts +29 -0
  44. package/dist/solana/rpc.js +96 -0
  45. package/dist/strategies/hold.d.ts +3 -0
  46. package/dist/strategies/hold.js +6 -0
  47. package/dist/strategies/ladder.d.ts +8 -0
  48. package/dist/strategies/ladder.js +51 -0
  49. package/dist/strategies/load.d.ts +5 -0
  50. package/dist/strategies/load.js +44 -0
  51. package/dist/strategies/preGrad.d.ts +10 -0
  52. package/dist/strategies/preGrad.js +32 -0
  53. package/dist/strategies/time.d.ts +6 -0
  54. package/dist/strategies/time.js +16 -0
  55. package/dist/strategies/tpsl.d.ts +8 -0
  56. package/dist/strategies/tpsl.js +30 -0
  57. package/dist/strategies/types.d.ts +55 -0
  58. package/dist/strategies/types.js +31 -0
  59. package/examples/dry.json +32 -0
  60. package/examples/live-embedded.json +14 -0
  61. package/examples/live-linked.json +15 -0
  62. package/examples/strategies/breakeven-stop.js +22 -0
  63. package/examples/strategies/stale-coin.js +18 -0
  64. package/examples/strategies/timeboxed-tp.js +18 -0
  65. package/package.json +60 -4
  66. package/skills/candle-launch-bot/SKILL.md +71 -0
@@ -0,0 +1,111 @@
1
+ import { type Rpc } from "../solana/rpc.ts";
2
+ import type { Logger } from "../log.ts";
3
+ export type Source = "api" | "chain";
4
+ export interface Launch {
5
+ chain: string;
6
+ mint: string;
7
+ source: Source;
8
+ /** When the launch happened: the launch tx's block time, else Candle's row creation time (ms). */
9
+ launchedAtMs: number | null;
10
+ detectedAtMs: number;
11
+ symbol?: string;
12
+ name?: string;
13
+ /** The token list's creator (api). */
14
+ creator?: string;
15
+ /** The launch transaction's fee payer (chain); on Candle often a payer wallet, not the token list's creator. */
16
+ signer?: string;
17
+ launchMode?: string;
18
+ signature?: string;
19
+ curve?: string;
20
+ decimals?: number;
21
+ }
22
+ export interface Sighting {
23
+ mint: string;
24
+ source: Source;
25
+ atMs: number;
26
+ launchedAtMs: number | null;
27
+ latencyMs: number | null;
28
+ }
29
+ export declare const TOKENS_PATH = "/api/v1/tokens?visibility=production&limit=";
30
+ export declare const INIT_LOG = "Program log: Instruction: InitializePool";
31
+ /** Candle token-list rows -> launches (Solana rows only; others are skipped until their source exists). */
32
+ export declare function fromApiRows(rows: unknown, nowMs: number): Launch[];
33
+ /** A transaction under Candle's curve program -> the launch it created, or null. */
34
+ export declare function fromLaunchTx(tx: any, signature: string, nowMs: number): Launch | null;
35
+ export declare function metadataPda(mint: string): string;
36
+ /** (name, symbol) from a Metaplex metadata account. */
37
+ export declare function readMetadata(base64: string): {
38
+ name: string;
39
+ symbol: string;
40
+ } | null;
41
+ export interface DetectorDeps {
42
+ rpc: Rpc;
43
+ wsUrl?: string;
44
+ apiUrl: string;
45
+ fetch?: typeof fetch;
46
+ log: Logger;
47
+ now?: () => number;
48
+ WebSocketImpl?: typeof WebSocket;
49
+ }
50
+ export interface DetectorOptions {
51
+ api: boolean;
52
+ chain: boolean;
53
+ apiPollMs: number;
54
+ chainPollMs: number;
55
+ apiLimit: number;
56
+ }
57
+ /**
58
+ * Collects launches; `drain()` hands the new ones to the engine. Sources run on their own timers; nothing in a
59
+ * source can throw into the engine loop.
60
+ */
61
+ export declare class Detector {
62
+ private seen;
63
+ private sightings;
64
+ private queue;
65
+ private sigSeen;
66
+ private sigOrder;
67
+ private timers;
68
+ private socket;
69
+ private polledOnce;
70
+ private inflight;
71
+ /** Signatures whose launch transaction could not be fetched yet: retried on every poll (bounded). */
72
+ private retries;
73
+ private lastSigPoll;
74
+ readonly stats: {
75
+ api: {
76
+ polls: number;
77
+ errors: number;
78
+ lastOkMs: number;
79
+ };
80
+ chain: {
81
+ events: number;
82
+ polls: number;
83
+ errors: number;
84
+ wsUp: boolean;
85
+ lastOkMs: number;
86
+ };
87
+ };
88
+ private deps;
89
+ private opts;
90
+ private now;
91
+ constructor(deps: DetectorDeps, opts: DetectorOptions);
92
+ /** Prime both sources with what exists now (marked seen, never reported) and start watching. */
93
+ start(): Promise<void>;
94
+ stop(): void;
95
+ drain(): Launch[];
96
+ sightingsFor(mint: string): Partial<Record<Source, Sighting>>;
97
+ /** Record a sighting; returns the launch when this is the first time the mint is seen. */
98
+ offer(l: Launch, opts?: {
99
+ prime?: boolean;
100
+ }): Launch | null;
101
+ get(mint: string): Launch | undefined;
102
+ pollApi(): Promise<void>;
103
+ private markSig;
104
+ private onLogs;
105
+ private fetchLaunch;
106
+ private enrich;
107
+ /** Signature polling: the fallback while the socket is down, and a backstop while it is up. */
108
+ private polling;
109
+ pollSignatures(prime: boolean): Promise<void>;
110
+ private pollSignaturesOnce;
111
+ }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Launch detection from two independent sources, deduped by mint:
3
+ * - api: Candle's public token list, newest first (no key).
4
+ * - chain: logsSubscribe on Candle's launch-curve program; an `InitializePool` instruction is a new launch.
5
+ * Signature polling (getSignaturesForAddress) is the fallback while the socket is down, and a slow
6
+ * backstop while it is up.
7
+ * Each mint is reported once, by whichever source saw it first; the other source's sighting is recorded too,
8
+ * so detection latency (launch tx time to sighting) is known for both.
9
+ *
10
+ * Chains other than Solana plug in as further sources with their own `chain` value.
11
+ */
12
+ import { asAddress } from "../solana/base58.js";
13
+ import { CANDLE_CURVE_PROGRAM, WSOL, candleCurve, findPda } from "../solana/pda.js";
14
+ import { b58decode } from "../solana/base58.js";
15
+ import { getTransaction } from "../solana/rpc.js";
16
+ import { subscribeLogs } from "../solana/logs-ws.js";
17
+ export const TOKENS_PATH = "/api/v1/tokens?visibility=production&limit=";
18
+ const METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
19
+ export const INIT_LOG = "Program log: Instruction: InitializePool";
20
+ /** Candle token-list rows -> launches (Solana rows only; others are skipped until their source exists). */
21
+ export function fromApiRows(rows, nowMs) {
22
+ if (!Array.isArray(rows))
23
+ return [];
24
+ const out = [];
25
+ for (const r of rows) {
26
+ const mint = asAddress(r?.mint);
27
+ if (!mint || (r.chain && r.chain !== "solana"))
28
+ continue;
29
+ if (r.quoteMint && r.quoteMint !== WSOL)
30
+ continue;
31
+ out.push({
32
+ chain: "solana",
33
+ mint,
34
+ source: "api",
35
+ launchedAtMs: typeof r._creationTime === "number" ? Math.round(r._creationTime) : null,
36
+ detectedAtMs: nowMs,
37
+ symbol: typeof r.symbol === "string" ? r.symbol : undefined,
38
+ name: typeof r.name === "string" ? r.name : undefined,
39
+ creator: asAddress(r.creator) ?? undefined,
40
+ launchMode: typeof r.launchMode === "string" ? r.launchMode : undefined,
41
+ curve: asAddress(r.virtualPool) ?? undefined,
42
+ decimals: typeof r.decimals === "number" ? r.decimals : undefined,
43
+ });
44
+ }
45
+ return out;
46
+ }
47
+ function accountKeys(tx) {
48
+ const msg = tx?.transaction?.message;
49
+ const keys = (msg?.accountKeys ?? []).map((k) => (typeof k === "string" ? k : k?.pubkey));
50
+ const loaded = tx?.meta?.loadedAddresses;
51
+ if (loaded)
52
+ keys.push(...(loaded.writable ?? []), ...(loaded.readonly ?? []));
53
+ return keys;
54
+ }
55
+ /** A transaction under Candle's curve program -> the launch it created, or null. */
56
+ export function fromLaunchTx(tx, signature, nowMs) {
57
+ const logs = tx?.meta?.logMessages ?? [];
58
+ if (tx?.meta?.err || !logs.includes(INIT_LOG))
59
+ return null;
60
+ const keys = accountKeys(tx);
61
+ if (!keys.includes(CANDLE_CURVE_PROGRAM))
62
+ return null;
63
+ for (const b of tx.meta.postTokenBalances ?? []) {
64
+ const mint = asAddress(b?.mint);
65
+ if (!mint || mint === WSOL)
66
+ continue;
67
+ const curve = candleCurve(mint);
68
+ if (!keys.includes(curve))
69
+ continue;
70
+ return {
71
+ chain: "solana",
72
+ mint,
73
+ source: "chain",
74
+ launchedAtMs: typeof tx.blockTime === "number" ? tx.blockTime * 1000 : null,
75
+ detectedAtMs: nowMs,
76
+ signer: keys[0],
77
+ signature,
78
+ curve,
79
+ decimals: typeof b?.uiTokenAmount?.decimals === "number" ? b.uiTokenAmount.decimals : undefined,
80
+ };
81
+ }
82
+ return null;
83
+ }
84
+ export function metadataPda(mint) {
85
+ const meta = b58decode(METADATA_PROGRAM);
86
+ return findPda([new TextEncoder().encode("metadata"), meta, b58decode(mint)], METADATA_PROGRAM);
87
+ }
88
+ /** (name, symbol) from a Metaplex metadata account. */
89
+ export function readMetadata(base64) {
90
+ try {
91
+ const d = Buffer.from(base64, "base64");
92
+ let at = 1 + 32 + 32;
93
+ const out = [];
94
+ for (let i = 0; i < 2; i++) {
95
+ const n = d.readUInt32LE(at);
96
+ at += 4;
97
+ if (n > 200)
98
+ return null;
99
+ out.push(d.subarray(at, at + n).toString("utf8").replace(/\0+$/, ""));
100
+ at += n;
101
+ }
102
+ return { name: out[0], symbol: out[1] };
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Collects launches; `drain()` hands the new ones to the engine. Sources run on their own timers; nothing in a
110
+ * source can throw into the engine loop.
111
+ */
112
+ export class Detector {
113
+ seen = new Map();
114
+ sightings = new Map();
115
+ queue = [];
116
+ sigSeen = new Set();
117
+ sigOrder = [];
118
+ timers = [];
119
+ socket = null;
120
+ polledOnce = false;
121
+ inflight = new Set();
122
+ /** Signatures whose launch transaction could not be fetched yet: retried on every poll (bounded). */
123
+ retries = new Map();
124
+ lastSigPoll = 0;
125
+ stats = { api: { polls: 0, errors: 0, lastOkMs: 0 }, chain: { events: 0, polls: 0, errors: 0, wsUp: false, lastOkMs: 0 } };
126
+ deps;
127
+ opts;
128
+ now;
129
+ constructor(deps, opts) {
130
+ this.deps = deps;
131
+ this.opts = opts;
132
+ this.now = deps.now ?? Date.now;
133
+ }
134
+ /** Prime both sources with what exists now (marked seen, never reported) and start watching. */
135
+ async start() {
136
+ if (this.opts.chain) {
137
+ await this.pollSignatures(true).catch(() => undefined);
138
+ if (this.deps.wsUrl) {
139
+ this.socket = subscribeLogs(this.deps.wsUrl, CANDLE_CURVE_PROGRAM, {
140
+ onLogs: (e) => void this.onLogs(e),
141
+ onState: (up, why) => {
142
+ this.stats.chain.wsUp = up;
143
+ this.deps.log.info(up ? "chain_ws_up" : "chain_ws_down", why ? { why } : {});
144
+ },
145
+ }, { WebSocketImpl: this.deps.WebSocketImpl });
146
+ }
147
+ // Poll fast while the socket is down, slowly as a backstop while it is up.
148
+ this.timers.push(setInterval(() => {
149
+ const every = this.stats.chain.wsUp ? this.opts.chainPollMs : Math.min(this.opts.chainPollMs, 3000);
150
+ if (this.now() - this.lastSigPoll >= every)
151
+ void this.pollSignatures(false).catch(() => undefined);
152
+ }, 1000));
153
+ }
154
+ if (this.opts.api) {
155
+ await this.pollApi().catch(() => undefined);
156
+ this.timers.push(setInterval(() => void this.pollApi().catch(() => undefined), this.opts.apiPollMs));
157
+ }
158
+ }
159
+ stop() {
160
+ for (const t of this.timers)
161
+ clearInterval(t);
162
+ this.timers = [];
163
+ this.socket?.close();
164
+ }
165
+ drain() {
166
+ const q = this.queue;
167
+ this.queue = [];
168
+ return q;
169
+ }
170
+ sightingsFor(mint) {
171
+ return this.sightings.get(mint) ?? {};
172
+ }
173
+ /** Record a sighting; returns the launch when this is the first time the mint is seen. */
174
+ offer(l, opts = {}) {
175
+ const prev = this.seen.get(l.mint);
176
+ const s = this.sightings.get(l.mint) ?? {};
177
+ if (!s[l.source]) {
178
+ const launchedAtMs = l.source === "chain" ? l.launchedAtMs : (prev?.source === "chain" ? prev.launchedAtMs : l.launchedAtMs);
179
+ s[l.source] = { mint: l.mint, source: l.source, atMs: l.detectedAtMs, launchedAtMs, latencyMs: launchedAtMs ? l.detectedAtMs - launchedAtMs : null };
180
+ this.sightings.set(l.mint, s);
181
+ // Latency is measured from the launch transaction's block time for every source: when the chain's time
182
+ // arrives after the api sighting (which used Candle's row time), recompute the api sighting from it.
183
+ if (l.source === "chain" && l.launchedAtMs && s.api) {
184
+ s.api.launchedAtMs = l.launchedAtMs;
185
+ s.api.latencyMs = s.api.atMs - l.launchedAtMs;
186
+ }
187
+ if (!opts.prime && prev)
188
+ this.deps.log.info("launch_seen_again", { mint: l.mint, source: l.source, latencyMs: s[l.source].latencyMs, firstSource: prev.source });
189
+ }
190
+ if (prev) {
191
+ // Fill in what the first sighting lacked (the chain has no launch mode; the api has no signature).
192
+ for (const k of ["symbol", "name", "creator", "signer", "launchMode", "signature", "curve", "decimals"])
193
+ if (prev[k] === undefined && l[k] !== undefined)
194
+ prev[k] = l[k];
195
+ if (l.source === "chain" && l.launchedAtMs)
196
+ prev.launchedAtMs = l.launchedAtMs;
197
+ return null;
198
+ }
199
+ this.seen.set(l.mint, l);
200
+ if (opts.prime)
201
+ return null;
202
+ this.queue.push(l);
203
+ return l;
204
+ }
205
+ get(mint) {
206
+ return this.seen.get(mint);
207
+ }
208
+ async pollApi() {
209
+ const f = this.deps.fetch ?? fetch;
210
+ this.stats.api.polls++;
211
+ try {
212
+ const res = await f(this.deps.apiUrl + TOKENS_PATH + this.opts.apiLimit, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
213
+ if (!res.ok)
214
+ throw new Error(`HTTP ${res.status}`);
215
+ const body = (await res.json());
216
+ const rows = Array.isArray(body) ? body : body?.payload;
217
+ // Not primed: rows that were already there at start reach the filter and fail its age check once each.
218
+ for (const l of fromApiRows(rows, this.now()))
219
+ this.offer(l);
220
+ this.stats.api.lastOkMs = this.now();
221
+ }
222
+ catch (e) {
223
+ this.stats.api.errors++;
224
+ if (this.stats.api.errors % 20 === 1)
225
+ this.deps.log.warn("api_poll_failed", { error: String(e.message ?? e), errors: this.stats.api.errors });
226
+ }
227
+ }
228
+ markSig(sig) {
229
+ if (this.sigSeen.has(sig))
230
+ return false;
231
+ this.sigSeen.add(sig);
232
+ this.sigOrder.push(sig);
233
+ if (this.sigOrder.length > 5000)
234
+ this.sigSeen.delete(this.sigOrder.shift());
235
+ return true;
236
+ }
237
+ async onLogs(e) {
238
+ this.stats.chain.events++;
239
+ // Every signature the socket delivers is marked, so the poller only fetches what the socket missed.
240
+ const fresh = this.markSig(e.signature);
241
+ if (!fresh || e.err || !e.logs.includes(INIT_LOG))
242
+ return;
243
+ await this.fetchLaunch(e.signature);
244
+ }
245
+ async fetchLaunch(sig) {
246
+ if (this.inflight.has(sig))
247
+ return;
248
+ this.inflight.add(sig);
249
+ try {
250
+ // A just-confirmed transaction can take a moment to be served by getTransaction.
251
+ for (let i = 0; i < 3; i++) {
252
+ let tx = null;
253
+ try {
254
+ tx = await getTransaction(this.deps.rpc, sig);
255
+ }
256
+ catch {
257
+ tx = null;
258
+ }
259
+ if (tx) {
260
+ this.retries.delete(sig);
261
+ const l = fromLaunchTx(tx, sig, this.now());
262
+ if (l) {
263
+ await this.enrich(l);
264
+ this.stats.chain.lastOkMs = this.now();
265
+ this.offer(l);
266
+ }
267
+ return;
268
+ }
269
+ await new Promise((r) => setTimeout(r, 300 * (i + 1)));
270
+ }
271
+ this.stats.chain.errors++;
272
+ const n = (this.retries.get(sig) ?? 0) + 1;
273
+ if (n <= 5)
274
+ this.retries.set(sig, n);
275
+ else
276
+ this.retries.delete(sig);
277
+ this.deps.log.warn("chain_tx_unavailable", { signature: sig, tries: n, willRetry: n <= 5 });
278
+ }
279
+ finally {
280
+ this.inflight.delete(sig);
281
+ }
282
+ }
283
+ async enrich(l) {
284
+ try {
285
+ const r = await this.deps.rpc.call("getAccountInfo", [metadataPda(l.mint), { encoding: "base64", commitment: "confirmed" }]);
286
+ const md = r?.value?.data?.[0] ? readMetadata(r.value.data[0]) : null;
287
+ if (md) {
288
+ l.name = md.name;
289
+ l.symbol = md.symbol;
290
+ }
291
+ }
292
+ catch {
293
+ /* name/symbol stay unknown; filters that need them wait or refuse */
294
+ }
295
+ }
296
+ /** Signature polling: the fallback while the socket is down, and a backstop while it is up. */
297
+ polling = false;
298
+ async pollSignatures(prime) {
299
+ // Single flight: a slow poll is never joined by a second one that would fetch the same transactions.
300
+ if (this.polling)
301
+ return;
302
+ this.polling = true;
303
+ try {
304
+ await this.pollSignaturesOnce(prime);
305
+ }
306
+ finally {
307
+ this.polling = false;
308
+ }
309
+ }
310
+ async pollSignaturesOnce(prime) {
311
+ this.stats.chain.polls++;
312
+ this.lastSigPoll = this.now();
313
+ const sigs = [];
314
+ try {
315
+ // Page back with `before` until a page reaches a signature already seen (or 5 pages): a burst of more than
316
+ // one page between polls is not skipped.
317
+ let before;
318
+ for (let page = 0; page < (prime ? 1 : 5); page++) {
319
+ const got = (await this.deps.rpc.call("getSignaturesForAddress", [CANDLE_CURVE_PROGRAM, { limit: prime ? 100 : 50, commitment: "confirmed", ...(before ? { before } : {}) }])) ?? [];
320
+ sigs.push(...got);
321
+ if (got.length < (prime ? 100 : 50) || got.some((x) => this.sigSeen.has(x?.signature)))
322
+ break;
323
+ before = got[got.length - 1]?.signature;
324
+ if (!before)
325
+ break;
326
+ }
327
+ }
328
+ catch (e) {
329
+ this.stats.chain.errors++;
330
+ return;
331
+ }
332
+ for (const sig of [...this.retries.keys()])
333
+ await this.fetchLaunch(sig);
334
+ const fresh = sigs.filter((s) => s && typeof s.signature === "string" && !s.err).reverse();
335
+ if (prime || !this.polledOnce) {
336
+ for (const s of fresh)
337
+ this.markSig(s.signature);
338
+ this.polledOnce = true;
339
+ return;
340
+ }
341
+ for (const s of fresh) {
342
+ if (!this.markSig(s.signature))
343
+ continue;
344
+ // No logs on this path: fetch each unseen transaction (the program's traffic is light).
345
+ await this.fetchLaunch(s.signature);
346
+ }
347
+ }
348
+ }
@@ -0,0 +1,103 @@
1
+ import type { Config } from "./config.ts";
2
+ import type { Detector } from "./detect/detector.ts";
3
+ import { Executor } from "./exec/executor.ts";
4
+ import type { Ledger, PositionRow } from "./ledger.ts";
5
+ import type { Logger } from "./log.ts";
6
+ import { type Rpc } from "./solana/rpc.ts";
7
+ import type { ExitStrategy } from "./strategies/types.ts";
8
+ import type { Alerter } from "./alerts.ts";
9
+ export interface MarketInfoSource {
10
+ /** Candle's public market record: launch mode (`tier`), name/symbol and decimals for a chain-first launch. */
11
+ getMarket(chain: "solana", mint: string): Promise<any>;
12
+ }
13
+ export interface EngineDeps {
14
+ cfg: Config;
15
+ ledger: Ledger;
16
+ detector: Detector;
17
+ executor: Executor;
18
+ strategy: ExitStrategy;
19
+ rpc: Rpc;
20
+ log: Logger;
21
+ alerter: Alerter;
22
+ markets?: MarketInfoSource | null;
23
+ now?: () => number;
24
+ }
25
+ /** Most time one cycle spends before the rest of its work waits for the next one (exits always run). */
26
+ export declare const CYCLE_BUDGET_MS = 20000;
27
+ /** Candle's market record while filtering a launch: a slow answer must not hold the loop. */
28
+ export declare const MARKET_READ_TIMEOUT_MS = 3000;
29
+ /** A DAMM v2 pool this close to its floor has no liquidity left to sell into. */
30
+ /** How often the wallet's balance of each held coin is compared with what this bot booked. */
31
+ export declare const SURPLUS_CHECK_MS = 600000;
32
+ /** Coins whose position ended this recently are still checked for a surplus. */
33
+ export declare const SURPLUS_TAIL_MS = 3600000;
34
+ export declare const FLOOR_ROOM_MIN = 0.005;
35
+ /** Read a control file without ever blocking or throwing: regular files only, non-blocking open, bounded read. */
36
+ export declare function readControl(path: string): string | null;
37
+ export declare class Engine {
38
+ private d;
39
+ private now;
40
+ private pending;
41
+ private exitHoldUntil;
42
+ /** When each position's exits were first held for want of a sell route (cleared once a route is back). */
43
+ private noRouteSince;
44
+ /** Next surplus check per coin. */
45
+ private surplusAt;
46
+ private surplusSeen;
47
+ private markets;
48
+ private stopRequested;
49
+ private cycles;
50
+ private lastCycleMs;
51
+ readonly stats: {
52
+ launches: number;
53
+ buys: number;
54
+ skips: number;
55
+ sells: number;
56
+ };
57
+ constructor(d: EngineDeps);
58
+ get stopping(): boolean;
59
+ requestStop(): void;
60
+ paused(): boolean;
61
+ /** One full cycle. Returns false when STOP was seen (the caller exits cleanly). */
62
+ /** STOP file or SIGTERM/SIGINT: checked before every step and between orders. */
63
+ shouldStop: () => boolean;
64
+ /** Each cycle has a time budget; work left over waits for the next cycle. */
65
+ private overBudget;
66
+ cycle(): Promise<boolean>;
67
+ private guard;
68
+ /** Conditions that need a human re-announce (alerts.renotifyMs) until they clear. */
69
+ private nag;
70
+ /**
71
+ * The wallet holding more of a coin than this bot booked (a second execution that landed after the first was booked,
72
+ * or tokens from elsewhere): alerted, never adopted. Checked per coin every SURPLUS_CHECK_MS, while no order of the
73
+ * coin is in flight.
74
+ */
75
+ private surplus;
76
+ private get dry();
77
+ /** Positions of this run's mode only: a dry run never manages live rows, a live run never dry ones. */
78
+ private mine;
79
+ private openCount;
80
+ private entries;
81
+ private decide;
82
+ private buy;
83
+ private view;
84
+ private exits;
85
+ /** Create a sell order clamped to the on-chain balance. Returns the order id, or null when refused. */
86
+ placeSell(p: PositionRow, fraction: number, reason: string, manual: boolean): Promise<number | null>;
87
+ private manualRequests;
88
+ /** The `sell` command's path: same clamp, same executor. Moves an exit_stuck position back to open. */
89
+ manualSell(mint: string, fraction: number): Promise<string>;
90
+ /**
91
+ * `sell` on a coin whose buy ended failed or rejected while the wallet holds its tokens (a buy that landed after the
92
+ * bot gave up, or that it could not prove): reopen that position with the wallet's unexplained tokens (qty from the
93
+ * wallet, cost the order's ticket) so the sell can run. Only buys that may have executed qualify. Alerted.
94
+ */
95
+ private reopenFailedBuy;
96
+ statusObject(): Record<string, unknown>;
97
+ writeStatus(): void;
98
+ }
99
+ /** One process per state dir. */
100
+ export declare function acquireLock(stateDir: string): {
101
+ release(): void;
102
+ } | null;
103
+ export declare function lockHeld(stateDir: string): boolean;