@solard/cli 0.2.19 → 0.2.20
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/package.json +2 -2
- package/src/index.ts +695 -146
- package/src/launch-watch-command.ts +278 -0
- package/src/price-feed-client.ts +2 -0
- package/src/price-feed-protocol.ts +8 -0
- package/src/price-feed-server.ts +1595 -0
- package/src/pump-watch-command.ts +11 -0
- package/src/reclaim-command.ts +335 -0
- package/src/research-history-cache.ts +331 -0
- package/src/strategy-backtest-command.ts +740 -0
- package/src/token-history-command.ts +183 -30
- package/src/trade-strategy-command.ts +1247 -0
- package/src/tx-stream.test.ts +38 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PublicKey, type ConfirmedSignatureInfo } from "@solana/web3.js";
|
|
1
2
|
import type { Solard } from "@solard/core";
|
|
2
3
|
import {
|
|
3
4
|
analyzeTokenHistory,
|
|
@@ -9,6 +10,12 @@ import {
|
|
|
9
10
|
type TokenHistoryTrade,
|
|
10
11
|
} from "@solard/core";
|
|
11
12
|
|
|
13
|
+
import {
|
|
14
|
+
replaceResearchHistoryIndex,
|
|
15
|
+
researchHistoryCachePath,
|
|
16
|
+
type ResearchHistorySignature,
|
|
17
|
+
} from "./research-history-cache.ts";
|
|
18
|
+
|
|
12
19
|
export type TokenHistoryCliFlags = Map<string, string>;
|
|
13
20
|
|
|
14
21
|
type Emit = (value: string) => void;
|
|
@@ -135,12 +142,170 @@ function tradeLine(
|
|
|
135
142
|
);
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
async function scanHistoryAddress(args: {
|
|
146
|
+
slrd: Solard;
|
|
147
|
+
kind: "curve" | "pool";
|
|
148
|
+
address: string;
|
|
149
|
+
maxSignatures: number;
|
|
150
|
+
progress: (line: string) => void;
|
|
151
|
+
}): Promise<{
|
|
152
|
+
rows: ResearchHistorySignature[];
|
|
153
|
+
reachedStart: boolean;
|
|
154
|
+
truncated: boolean;
|
|
155
|
+
}> {
|
|
156
|
+
const rows: ConfirmedSignatureInfo[] = [];
|
|
157
|
+
let before: string | undefined;
|
|
158
|
+
let pages = 0;
|
|
159
|
+
let reachedStart = false;
|
|
160
|
+
let truncated = false;
|
|
161
|
+
while (true) {
|
|
162
|
+
const remaining =
|
|
163
|
+
args.maxSignatures > 0 ? args.maxSignatures - rows.length : 1_000;
|
|
164
|
+
if (args.maxSignatures > 0 && remaining <= 0) {
|
|
165
|
+
truncated = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
const limit = Math.min(1_000, args.maxSignatures > 0 ? remaining : 1_000);
|
|
169
|
+
const page = await args.slrd
|
|
170
|
+
.connection()
|
|
171
|
+
.getSignaturesForAddress(
|
|
172
|
+
new PublicKey(args.address),
|
|
173
|
+
{ limit, ...(before ? { before } : {}) },
|
|
174
|
+
"finalized",
|
|
175
|
+
);
|
|
176
|
+
pages += 1;
|
|
177
|
+
rows.push(...page.filter((row) => !row.err));
|
|
178
|
+
if (pages === 1 || pages % 5 === 0)
|
|
179
|
+
args.progress(
|
|
180
|
+
`SIGS ${args.kind.padEnd(5)} pages=${pages} signatures=${rows.length}`,
|
|
181
|
+
);
|
|
182
|
+
if (page.length < limit) {
|
|
183
|
+
reachedStart = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const next = page.at(-1)?.signature;
|
|
187
|
+
if (!next || next === before) {
|
|
188
|
+
reachedStart = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
before = next;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
rows: [...rows].reverse().map((row, index) => ({
|
|
195
|
+
mint: "",
|
|
196
|
+
signature: row.signature,
|
|
197
|
+
slot: row.slot,
|
|
198
|
+
blockTime: row.blockTime ?? null,
|
|
199
|
+
scanKind: args.kind,
|
|
200
|
+
scanAddress: args.address,
|
|
201
|
+
localChronologicalOrder: index,
|
|
202
|
+
})),
|
|
203
|
+
reachedStart,
|
|
204
|
+
truncated,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runIndexBackfill(args: {
|
|
209
|
+
slrd: Solard;
|
|
210
|
+
mint: string;
|
|
211
|
+
flags: TokenHistoryCliFlags;
|
|
212
|
+
emit: Emit;
|
|
213
|
+
}): Promise<void> {
|
|
214
|
+
const jsonMode = args.flags.has("json");
|
|
215
|
+
const progress = (line: string) => {
|
|
216
|
+
if (!jsonMode) process.stderr.write(`${line}\n`);
|
|
217
|
+
};
|
|
218
|
+
progress(`TOKEN INDEX ${args.mint}`);
|
|
219
|
+
const token = await args.slrd.addToken(args.mint);
|
|
220
|
+
if (!token.bondingCurve)
|
|
221
|
+
throw new Error(`Token ${args.mint} has no Pump bonding curve to index`);
|
|
222
|
+
const supply = await args.slrd
|
|
223
|
+
.connection()
|
|
224
|
+
.getTokenSupply(new PublicKey(args.mint), "confirmed");
|
|
225
|
+
const maxSignatures = Math.max(
|
|
226
|
+
0,
|
|
227
|
+
Math.trunc(numberFlag(args.flags, "max-signatures", 0) ?? 0),
|
|
228
|
+
);
|
|
229
|
+
const curve = await scanHistoryAddress({
|
|
230
|
+
slrd: args.slrd,
|
|
231
|
+
kind: "curve",
|
|
232
|
+
address: token.bondingCurve,
|
|
233
|
+
maxSignatures,
|
|
234
|
+
progress,
|
|
235
|
+
});
|
|
236
|
+
const pool = token.pool
|
|
237
|
+
? await scanHistoryAddress({
|
|
238
|
+
slrd: args.slrd,
|
|
239
|
+
kind: "pool",
|
|
240
|
+
address: token.pool,
|
|
241
|
+
maxSignatures,
|
|
242
|
+
progress,
|
|
243
|
+
})
|
|
244
|
+
: null;
|
|
245
|
+
const unique = new Map<string, ResearchHistorySignature>();
|
|
246
|
+
for (const row of [...curve.rows, ...(pool?.rows ?? [])]) {
|
|
247
|
+
const value = { ...row, mint: args.mint };
|
|
248
|
+
const previous = unique.get(value.signature);
|
|
249
|
+
if (!previous || value.scanKind === "curve")
|
|
250
|
+
unique.set(value.signature, value);
|
|
251
|
+
}
|
|
252
|
+
const rows = [...unique.values()].sort(
|
|
253
|
+
(a, b) =>
|
|
254
|
+
a.slot - b.slot ||
|
|
255
|
+
(a.blockTime ?? 0) - (b.blockTime ?? 0) ||
|
|
256
|
+
a.localChronologicalOrder - b.localChronologicalOrder ||
|
|
257
|
+
a.signature.localeCompare(b.signature),
|
|
258
|
+
);
|
|
259
|
+
rows.forEach((row, index) => {
|
|
260
|
+
row.localChronologicalOrder = index;
|
|
261
|
+
});
|
|
262
|
+
const fromCreation = curve.reachedStart && !curve.truncated;
|
|
263
|
+
const meta = {
|
|
264
|
+
version: 1 as const,
|
|
265
|
+
mint: args.mint,
|
|
266
|
+
decimals: supply.value.decimals,
|
|
267
|
+
supplyUi: Number(supply.value.uiAmountString ?? "0"),
|
|
268
|
+
quoteMint: token.quoteMint ?? null,
|
|
269
|
+
bondingCurve: token.bondingCurve,
|
|
270
|
+
pool: token.pool ?? null,
|
|
271
|
+
fromCreation,
|
|
272
|
+
indexedSignatures: rows.length,
|
|
273
|
+
indexedAtMs: Date.now(),
|
|
274
|
+
};
|
|
275
|
+
replaceResearchHistoryIndex({ mint: args.mint, rows, meta });
|
|
276
|
+
const result = {
|
|
277
|
+
...meta,
|
|
278
|
+
curveSignatures: curve.rows.length,
|
|
279
|
+
poolSignatures: pool?.rows.length ?? 0,
|
|
280
|
+
cache: researchHistoryCachePath(),
|
|
281
|
+
};
|
|
282
|
+
if (jsonMode) args.emit(`${JSON.stringify(result)}\n`);
|
|
283
|
+
else
|
|
284
|
+
args.emit(
|
|
285
|
+
[
|
|
286
|
+
"",
|
|
287
|
+
"TOKEN HISTORY INDEX",
|
|
288
|
+
`Mint: ${args.mint}`,
|
|
289
|
+
`Signatures: ${rows.length}`,
|
|
290
|
+
`From creation: ${fromCreation ? "yes" : "no"}`,
|
|
291
|
+
`Transactions read: 0`,
|
|
292
|
+
`Research cache: ${researchHistoryCachePath()}`,
|
|
293
|
+
"",
|
|
294
|
+
"READY: scripts hydrate only the historical intervals they request.",
|
|
295
|
+
].join("\n") + "\n",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
138
299
|
async function runBackfill(args: {
|
|
139
300
|
slrd: Solard;
|
|
140
301
|
mint: string;
|
|
141
302
|
flags: TokenHistoryCliFlags;
|
|
142
303
|
emit: Emit;
|
|
143
304
|
}): Promise<void> {
|
|
305
|
+
if (!args.flags.has("exact") && !args.flags.has("materialize")) {
|
|
306
|
+
await runIndexBackfill(args);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
144
309
|
const jsonMode = args.flags.has("json");
|
|
145
310
|
const progress = (line: string) => {
|
|
146
311
|
if (!jsonMode) process.stderr.write(`${line}\n`);
|
|
@@ -157,53 +322,53 @@ async function runBackfill(args: {
|
|
|
157
322
|
rpcRetries: numberFlag(args.flags, "rpc-retries", 2),
|
|
158
323
|
retryDelayMs: numberFlag(args.flags, "retry-delay-ms", 750),
|
|
159
324
|
maxSignaturesPerAddress: numberFlag(args.flags, "max-signatures", 0),
|
|
325
|
+
priceSampleMs: args.flags.has("exact")
|
|
326
|
+
? 0
|
|
327
|
+
: numberFlag(args.flags, "price-sample-ms", 1_000),
|
|
160
328
|
replace: args.flags.has("replace"),
|
|
161
329
|
onProgress: (row) => {
|
|
162
330
|
if (row.phase === "signatures") {
|
|
163
|
-
if (row.pages === 1 || row.pages % 5 === 0)
|
|
331
|
+
if (row.pages === 1 || row.pages % 5 === 0)
|
|
164
332
|
progress(
|
|
165
333
|
`SIGS ${row.kind.padEnd(5)} pages=${row.pages} signatures=${row.signatures}`,
|
|
166
334
|
);
|
|
167
|
-
|
|
335
|
+
} else if (row.phase === "sample") {
|
|
336
|
+
progress(
|
|
337
|
+
`SAMPLE ${row.selected}/${row.total} signatures @ ${row.sampleMs}ms`,
|
|
338
|
+
);
|
|
168
339
|
} else if (row.phase === "transactions") {
|
|
169
340
|
if (
|
|
170
341
|
row.completed === row.total ||
|
|
171
342
|
row.completed <= row.batchSize ||
|
|
172
343
|
row.completed % 500 === 0
|
|
173
|
-
)
|
|
344
|
+
)
|
|
174
345
|
progress(`TX ${row.completed}/${row.total}`);
|
|
175
|
-
}
|
|
176
346
|
} else if (row.phase === "retry") {
|
|
177
347
|
progress(
|
|
178
|
-
`RETRY ${row.operation}${row.kind ? `:${row.kind}` : ""}
|
|
179
|
-
`${row.attempt}/${row.maxAttempts} ${row.error}`,
|
|
348
|
+
`RETRY ${row.operation}${row.kind ? `:${row.kind}` : ""} ${row.attempt}/${row.maxAttempts} ${row.error}`,
|
|
180
349
|
);
|
|
181
350
|
} else if (row.phase === "rpc-error") {
|
|
182
351
|
progress(
|
|
183
|
-
`RPCERR ${row.operation}${row.kind ? `:${row.kind}` : ""}
|
|
184
|
-
`failed=${row.failedItems} ${row.error}`,
|
|
352
|
+
`RPCERR ${row.operation}${row.kind ? `:${row.kind}` : ""} failed=${row.failedItems} ${row.error}`,
|
|
185
353
|
);
|
|
186
354
|
} else if (row.phase === "throttle") {
|
|
187
355
|
progress(
|
|
188
|
-
`THROT ${row.reason} wait=${row.waitMs}ms
|
|
189
|
-
`batch=${row.batchSize}->${row.nextBatchSize}`,
|
|
356
|
+
`THROT ${row.reason} wait=${row.waitMs}ms batch=${row.batchSize}->${row.nextBatchSize}`,
|
|
190
357
|
);
|
|
191
358
|
} else if (row.phase === "parse") {
|
|
192
359
|
if (
|
|
193
360
|
row.completed === row.total ||
|
|
194
361
|
row.completed === 1 ||
|
|
195
362
|
row.completed % 1_000 === 0
|
|
196
|
-
)
|
|
363
|
+
)
|
|
197
364
|
progress(
|
|
198
365
|
`PARSE ${row.completed}/${row.total} trades=${row.trades} ambiguous=${row.ambiguous}`,
|
|
199
366
|
);
|
|
200
|
-
}
|
|
201
367
|
} else if (row.phase === "store") {
|
|
202
|
-
if (row.completed === row.total || row.completed % 1_000 === 0)
|
|
368
|
+
if (row.completed === row.total || row.completed % 1_000 === 0)
|
|
203
369
|
progress(
|
|
204
370
|
`STORE ${row.completed}/${row.total} inserted=${row.inserted} updated=${row.updated}`,
|
|
205
371
|
);
|
|
206
|
-
}
|
|
207
372
|
} else if (row.phase === "candles") {
|
|
208
373
|
progress(`1S trades=${row.trades} sparse-candles=${row.candles}`);
|
|
209
374
|
}
|
|
@@ -225,28 +390,16 @@ async function runBackfill(args: {
|
|
|
225
390
|
args.emit(
|
|
226
391
|
[
|
|
227
392
|
"",
|
|
228
|
-
"TOKEN HISTORY
|
|
393
|
+
"TOKEN HISTORY MATERIALIZATION",
|
|
229
394
|
`Mint: ${result.mint}`,
|
|
230
395
|
`Coverage: ${coverageText(result)}`,
|
|
231
396
|
`Creation: ${fmtDate(result.creationAtMs)} ${result.creationSignature ? compact(result.creationSignature, 10, 10) : "-"}`,
|
|
232
|
-
result.venueFamily === "raydium"
|
|
233
|
-
? `LaunchLab pool: ${result.launchLabPool ?? "not found / history begins after launch"}`
|
|
234
|
-
: `Bonding curve: ${result.bondingCurve}`,
|
|
235
|
-
result.venueFamily === "raydium"
|
|
236
|
-
? `Raydium pools: ${(result.raydiumPools ?? []).join(", ") || "none discovered"}`
|
|
237
|
-
: `PumpSwap pool: ${result.pool ?? "not observed / not graduated"}`,
|
|
238
397
|
`Signatures: ${result.uniqueSignatures}`,
|
|
239
|
-
`
|
|
240
|
-
`
|
|
241
|
-
`Failed tx lookups: ${result.failedTransactions}`,
|
|
398
|
+
`History mode: ${result.historyMode ?? "exact"}${result.priceSampleMs ? ` @ ${result.priceSampleMs}ms` : ""}`,
|
|
399
|
+
`Hydrated tx: ${result.parsedTransactions}/${result.uniqueSignatures}`,
|
|
242
400
|
`Stored trades: ${result.storedTrades}`,
|
|
243
401
|
`Sparse 1s candles: ${result.storedCandles1s}`,
|
|
244
|
-
`Inserted/updated: ${result.insertedTrades}/${result.updatedTrades}`,
|
|
245
|
-
`Ambiguous skipped: ${result.skippedAmbiguous}`,
|
|
246
402
|
"",
|
|
247
|
-
result.complete
|
|
248
|
-
? "READY: durable tape is proven complete from creation."
|
|
249
|
-
: "WARNING: tape is not proven complete; inspect the gaps above before strict backtesting.",
|
|
250
403
|
].join("\n") + "\n",
|
|
251
404
|
);
|
|
252
405
|
}
|
|
@@ -514,7 +667,7 @@ export async function runTokenHistoryCommand(args: {
|
|
|
514
667
|
throw new Error(
|
|
515
668
|
`Usage: slrd token ${args.action} <CA> ${
|
|
516
669
|
args.action === "backfill"
|
|
517
|
-
? "[--
|
|
670
|
+
? "[--exact|--materialize --price-sample-ms 1000] [--max-signatures N] [--json]"
|
|
518
671
|
: args.action === "trades"
|
|
519
672
|
? "[--from-start] [--min-sol N] [--owner wallet] [--json]"
|
|
520
673
|
: "[--top N] [--first N] [--json]"
|