@solard/cli 0.2.13 → 0.2.15
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/backtest-batch-command.ts +262 -0
- package/src/backtest-command.ts +533 -0
- package/src/commands/buy.ts +207 -0
- package/src/commands/jupiter-swap.test.ts +78 -0
- package/src/commands/jupiter-swap.ts +269 -0
- package/src/commands/sell.ts +239 -0
- package/src/commands/trade-result.ts +19 -0
- package/src/commands/trade-targets.ts +43 -0
- package/src/commands/trade-venue.test.ts +32 -0
- package/src/commands/trade-venue.ts +19 -0
- package/src/commands/transfer.ts +200 -0
- package/src/fairfun-commands.ts +441 -0
- package/src/index.ts +387 -290
- package/src/meteora-commands.ts +148 -6
- package/src/pump/external-deployment-cli.ts +136 -0
- package/src/pump/token-launch-cli.ts +82 -6
- package/src/raydium-commands.ts +398 -0
- package/src/refs.ts +56 -0
- package/src/token-history-command.ts +559 -0
- package/src/vault-onboarding.ts +242 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import type { Solard } from "@solard/sdk";
|
|
2
|
+
import {
|
|
3
|
+
analyzeTokenHistory,
|
|
4
|
+
analyzeTokenHistoryForensicsFromStore,
|
|
5
|
+
backfillTokenHistory,
|
|
6
|
+
backfillRaydiumTokenHistory,
|
|
7
|
+
getTokenHistoryCoverage,
|
|
8
|
+
loadTokenHistoryTrades,
|
|
9
|
+
TokenHistoryError,
|
|
10
|
+
type BackfillTokenHistoryOptions,
|
|
11
|
+
type TokenHistoryCoverage,
|
|
12
|
+
type TokenHistoryTrade,
|
|
13
|
+
} from "@solard/sdk";
|
|
14
|
+
|
|
15
|
+
export type TokenHistoryCliFlags = Map<string, string>;
|
|
16
|
+
|
|
17
|
+
type Emit = (value: string) => void;
|
|
18
|
+
|
|
19
|
+
function flag(flags: TokenHistoryCliFlags, key: string): string | undefined {
|
|
20
|
+
const value = flags.get(key);
|
|
21
|
+
return value && value !== "true" ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function numberFlag(
|
|
25
|
+
flags: TokenHistoryCliFlags,
|
|
26
|
+
key: string,
|
|
27
|
+
fallback?: number,
|
|
28
|
+
): number | undefined {
|
|
29
|
+
const value = flag(flags, key);
|
|
30
|
+
if (value == null) return fallback;
|
|
31
|
+
const parsed = Number(value);
|
|
32
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${value}`);
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function json(value: unknown): string {
|
|
37
|
+
return JSON.stringify(
|
|
38
|
+
value,
|
|
39
|
+
(_, item) => (typeof item === "bigint" ? item.toString() : item),
|
|
40
|
+
2,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compact(address: string, left = 6, right = 6): string {
|
|
45
|
+
if (address.length <= left + right + 1) return address;
|
|
46
|
+
return `${address.slice(0, left)}…${address.slice(-right)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function aliases(slrd: Solard): Map<string, string> {
|
|
50
|
+
return new Map(
|
|
51
|
+
slrd.wallets
|
|
52
|
+
.list()
|
|
53
|
+
.map((wallet) => [wallet.address, `@${wallet.name}`] as const),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ownerLabel(owner: string | null, names: Map<string, string>): string {
|
|
58
|
+
if (!owner) return "-";
|
|
59
|
+
return names.get(owner) ?? compact(owner, 5, 5);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveOwner(slrd: Solard, value: string | undefined): string | null {
|
|
63
|
+
if (!value) return null;
|
|
64
|
+
try {
|
|
65
|
+
return slrd.resolveWallet(value).address.toBase58();
|
|
66
|
+
} catch {
|
|
67
|
+
return value.trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fmtNumber(value: number, digits = 6): string {
|
|
72
|
+
if (!Number.isFinite(value)) return "-";
|
|
73
|
+
if (Math.abs(value) >= 1_000_000) {
|
|
74
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
75
|
+
}
|
|
76
|
+
if (Math.abs(value) >= 1) {
|
|
77
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: digits });
|
|
78
|
+
}
|
|
79
|
+
if (value === 0) return "0";
|
|
80
|
+
if (Math.abs(value) >= 0.000001) return value.toFixed(9).replace(/0+$/, "");
|
|
81
|
+
return value.toExponential(4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function fmtDate(ms: number | null): string {
|
|
85
|
+
return ms == null ? "-" : new Date(ms).toISOString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fmtDelta(ms: number | null): string {
|
|
89
|
+
if (ms == null) return "-";
|
|
90
|
+
const sign = ms < 0 ? "-" : "+";
|
|
91
|
+
const value = Math.abs(ms);
|
|
92
|
+
if (value < 1_000) return `${sign}${value}ms`;
|
|
93
|
+
if (value < 60_000)
|
|
94
|
+
return `${sign}${(value / 1_000).toFixed(value % 1_000 === 0 ? 0 : 3)}s`;
|
|
95
|
+
if (value < 3_600_000) return `${sign}${(value / 60_000).toFixed(2)}m`;
|
|
96
|
+
return `${sign}${(value / 3_600_000).toFixed(2)}h`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function signedSol(value: number): string {
|
|
100
|
+
const sign = value > 0 ? "+" : value < 0 ? "-" : "";
|
|
101
|
+
return `${sign}${fmtNumber(Math.abs(value))}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function coverageText(
|
|
105
|
+
coverage: ReturnType<typeof getTokenHistoryCoverage>,
|
|
106
|
+
): string {
|
|
107
|
+
if (!coverage) return "NOT BACKFILLED";
|
|
108
|
+
if (coverage.complete && coverage.fromCreation)
|
|
109
|
+
return "COMPLETE FROM CREATION";
|
|
110
|
+
if (coverage.fromCreation) return "FROM CREATION, WITH RPC GAPS/TRUNCATION";
|
|
111
|
+
return "PARTIAL / CREATION NOT PROVEN";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function tradeLine(
|
|
115
|
+
row: TokenHistoryTrade,
|
|
116
|
+
index: number,
|
|
117
|
+
names: Map<string, string>,
|
|
118
|
+
): string {
|
|
119
|
+
const venue =
|
|
120
|
+
row.history.venue === "pumpswap"
|
|
121
|
+
? "AMM"
|
|
122
|
+
: row.history.venue === "raydium"
|
|
123
|
+
? "RAY"
|
|
124
|
+
: "CURVE";
|
|
125
|
+
const side = row.side.toUpperCase().padEnd(4);
|
|
126
|
+
const owner = ownerLabel(row.owner, names).padEnd(12);
|
|
127
|
+
const sol = `${fmtNumber(Math.abs(row.solDeltaUi), 6)} SOL`.padStart(16);
|
|
128
|
+
const tokens = fmtNumber(Math.abs(row.tokenDeltaUi), 4).padStart(16);
|
|
129
|
+
const price = row.priceSol == null ? "-" : fmtNumber(row.priceSol, 9);
|
|
130
|
+
const mcap =
|
|
131
|
+
row.history.marketCapSol == null
|
|
132
|
+
? "-"
|
|
133
|
+
: `${fmtNumber(row.history.marketCapSol, 2)} SOL`;
|
|
134
|
+
return (
|
|
135
|
+
`${String(index).padStart(6)} ${fmtDate(row.tradedAtMs)} ` +
|
|
136
|
+
`${venue.padEnd(5)} ${side} ${owner} ${sol} ${tokens} tok ` +
|
|
137
|
+
`px=${price} mcap=${mcap} ${compact(row.signature, 8, 8)}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runBackfill(args: {
|
|
142
|
+
slrd: Solard;
|
|
143
|
+
mint: string;
|
|
144
|
+
flags: TokenHistoryCliFlags;
|
|
145
|
+
emit: Emit;
|
|
146
|
+
}): Promise<void> {
|
|
147
|
+
const jsonMode = args.flags.has("json");
|
|
148
|
+
const progress = (line: string) => {
|
|
149
|
+
if (!jsonMode) process.stderr.write(`${line}\n`);
|
|
150
|
+
};
|
|
151
|
+
progress(`TOKEN BACKFILL ${args.mint}`);
|
|
152
|
+
const backfillOptions: BackfillTokenHistoryOptions = {
|
|
153
|
+
commitment: args.flags.has("confirmed")
|
|
154
|
+
? ("confirmed" as const)
|
|
155
|
+
: ("finalized" as const),
|
|
156
|
+
pageSize: numberFlag(args.flags, "page-size", 1_000),
|
|
157
|
+
transactionBatchSize: numberFlag(args.flags, "batch-size", 100),
|
|
158
|
+
transactionConcurrency: numberFlag(args.flags, "rpc-concurrency", 3),
|
|
159
|
+
rpcTimeoutMs: numberFlag(args.flags, "rpc-timeout-ms", 20_000),
|
|
160
|
+
rpcRetries: numberFlag(args.flags, "rpc-retries", 2),
|
|
161
|
+
retryDelayMs: numberFlag(args.flags, "retry-delay-ms", 750),
|
|
162
|
+
maxSignaturesPerAddress: numberFlag(args.flags, "max-signatures", 0),
|
|
163
|
+
replace: args.flags.has("replace"),
|
|
164
|
+
onProgress: (row) => {
|
|
165
|
+
if (row.phase === "signatures") {
|
|
166
|
+
if (row.pages === 1 || row.pages % 5 === 0) {
|
|
167
|
+
progress(
|
|
168
|
+
`SIGS ${row.kind.padEnd(5)} pages=${row.pages} signatures=${row.signatures}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
} else if (row.phase === "transactions") {
|
|
172
|
+
if (
|
|
173
|
+
row.completed === row.total ||
|
|
174
|
+
row.completed <= row.batchSize ||
|
|
175
|
+
row.completed % 500 === 0
|
|
176
|
+
) {
|
|
177
|
+
progress(`TX ${row.completed}/${row.total}`);
|
|
178
|
+
}
|
|
179
|
+
} else if (row.phase === "retry") {
|
|
180
|
+
progress(
|
|
181
|
+
`RETRY ${row.operation}${row.kind ? `:${row.kind}` : ""} ` +
|
|
182
|
+
`${row.attempt}/${row.maxAttempts} ${row.error}`,
|
|
183
|
+
);
|
|
184
|
+
} else if (row.phase === "rpc-error") {
|
|
185
|
+
progress(
|
|
186
|
+
`RPCERR ${row.operation}${row.kind ? `:${row.kind}` : ""} ` +
|
|
187
|
+
`failed=${row.failedItems} ${row.error}`,
|
|
188
|
+
);
|
|
189
|
+
} else if (row.phase === "throttle") {
|
|
190
|
+
progress(
|
|
191
|
+
`THROT ${row.reason} wait=${row.waitMs}ms ` +
|
|
192
|
+
`batch=${row.batchSize}->${row.nextBatchSize}`,
|
|
193
|
+
);
|
|
194
|
+
} else if (row.phase === "parse") {
|
|
195
|
+
if (
|
|
196
|
+
row.completed === row.total ||
|
|
197
|
+
row.completed === 1 ||
|
|
198
|
+
row.completed % 1_000 === 0
|
|
199
|
+
) {
|
|
200
|
+
progress(
|
|
201
|
+
`PARSE ${row.completed}/${row.total} trades=${row.trades} ambiguous=${row.ambiguous}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
} else if (row.phase === "store") {
|
|
205
|
+
if (row.completed === row.total || row.completed % 1_000 === 0) {
|
|
206
|
+
progress(
|
|
207
|
+
`STORE ${row.completed}/${row.total} inserted=${row.inserted} updated=${row.updated}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} else if (row.phase === "candles") {
|
|
211
|
+
progress(`1S trades=${row.trades} sparse-candles=${row.candles}`);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
let result: TokenHistoryCoverage;
|
|
216
|
+
try {
|
|
217
|
+
result = await backfillTokenHistory(
|
|
218
|
+
args.slrd.connection(),
|
|
219
|
+
args.mint,
|
|
220
|
+
backfillOptions,
|
|
221
|
+
);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (
|
|
224
|
+
!(error instanceof TokenHistoryError) ||
|
|
225
|
+
error.code !== "UNSUPPORTED_TOKEN"
|
|
226
|
+
)
|
|
227
|
+
throw error;
|
|
228
|
+
progress(
|
|
229
|
+
"VENUE Pump history unsupported; trying Raydium/LaunchLab discovery",
|
|
230
|
+
);
|
|
231
|
+
result = await backfillRaydiumTokenHistory(
|
|
232
|
+
args.slrd.connection(),
|
|
233
|
+
args.mint,
|
|
234
|
+
{
|
|
235
|
+
...backfillOptions,
|
|
236
|
+
maxRaydiumPools: Math.max(
|
|
237
|
+
1,
|
|
238
|
+
Math.trunc(numberFlag(args.flags, "raydium-pools", 8) ?? 8),
|
|
239
|
+
),
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (jsonMode) {
|
|
244
|
+
args.emit(`${json(result)}\n`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
args.emit(
|
|
248
|
+
[
|
|
249
|
+
"",
|
|
250
|
+
"TOKEN HISTORY BACKFILL",
|
|
251
|
+
`Mint: ${result.mint}`,
|
|
252
|
+
`Coverage: ${coverageText(result)}`,
|
|
253
|
+
`Creation: ${fmtDate(result.creationAtMs)} ${result.creationSignature ? compact(result.creationSignature, 10, 10) : "-"}`,
|
|
254
|
+
result.venueFamily === "raydium"
|
|
255
|
+
? `LaunchLab pool: ${result.launchLabPool ?? "not found / history begins after launch"}`
|
|
256
|
+
: `Bonding curve: ${result.bondingCurve}`,
|
|
257
|
+
result.venueFamily === "raydium"
|
|
258
|
+
? `Raydium pools: ${(result.raydiumPools ?? []).join(", ") || "none discovered"}`
|
|
259
|
+
: `PumpSwap pool: ${result.pool ?? "not observed / not graduated"}`,
|
|
260
|
+
`Signatures: ${result.uniqueSignatures}`,
|
|
261
|
+
`Parsed tx: ${result.parsedTransactions}`,
|
|
262
|
+
`Missing tx: ${result.missingTransactions}`,
|
|
263
|
+
`Failed tx lookups: ${result.failedTransactions}`,
|
|
264
|
+
`Stored trades: ${result.storedTrades}`,
|
|
265
|
+
`Sparse 1s candles: ${result.storedCandles1s}`,
|
|
266
|
+
`Inserted/updated: ${result.insertedTrades}/${result.updatedTrades}`,
|
|
267
|
+
`Ambiguous skipped: ${result.skippedAmbiguous}`,
|
|
268
|
+
"",
|
|
269
|
+
result.complete
|
|
270
|
+
? "READY: durable tape is proven complete from creation."
|
|
271
|
+
: "WARNING: tape is not proven complete; inspect the gaps above before strict backtesting.",
|
|
272
|
+
].join("\n") + "\n",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function runTrades(args: {
|
|
277
|
+
slrd: Solard;
|
|
278
|
+
mint: string;
|
|
279
|
+
flags: TokenHistoryCliFlags;
|
|
280
|
+
emit: Emit;
|
|
281
|
+
}): void {
|
|
282
|
+
const names = aliases(args.slrd);
|
|
283
|
+
const owner = resolveOwner(args.slrd, flag(args.flags, "owner"));
|
|
284
|
+
const side = flag(args.flags, "side");
|
|
285
|
+
if (side && side !== "buy" && side !== "sell") {
|
|
286
|
+
throw new Error("--side must be buy or sell");
|
|
287
|
+
}
|
|
288
|
+
const minSol = Math.max(0, numberFlag(args.flags, "min-sol", 0) ?? 0);
|
|
289
|
+
const fromStart = args.flags.has("from-start") || args.flags.has("oldest");
|
|
290
|
+
const explicitLimit = numberFlag(args.flags, "limit");
|
|
291
|
+
const limit =
|
|
292
|
+
explicitLimit == null
|
|
293
|
+
? fromStart
|
|
294
|
+
? Number.POSITIVE_INFINITY
|
|
295
|
+
: 100
|
|
296
|
+
: Math.max(1, Math.trunc(explicitLimit));
|
|
297
|
+
|
|
298
|
+
let rows = loadTokenHistoryTrades(args.mint).filter(
|
|
299
|
+
(row) =>
|
|
300
|
+
(!owner || row.owner === owner) &&
|
|
301
|
+
(!side || row.side === side) &&
|
|
302
|
+
Math.abs(row.solDeltaUi) >= minSol,
|
|
303
|
+
);
|
|
304
|
+
if (!fromStart) rows = rows.reverse();
|
|
305
|
+
rows = rows.slice(0, limit);
|
|
306
|
+
const coverage = getTokenHistoryCoverage(args.mint);
|
|
307
|
+
|
|
308
|
+
if (args.flags.has("json")) {
|
|
309
|
+
args.emit(`${json({ coverage, trades: rows })}\n`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
args.emit(`TOKEN TRADES ${args.mint}\n`);
|
|
314
|
+
args.emit(`Coverage: ${coverageText(coverage)}\n`);
|
|
315
|
+
if (coverage?.creationAtMs) {
|
|
316
|
+
args.emit(
|
|
317
|
+
`CREATE ${fmtDate(coverage.creationAtMs)} ${coverage.creationSignature ?? "-"}` +
|
|
318
|
+
`${coverage.creationSymbol ? ` $${coverage.creationSymbol}` : ""}\n`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (coverage?.pumpswap?.oldestBlockTime != null) {
|
|
322
|
+
args.emit(
|
|
323
|
+
`POOL ${fmtDate(coverage.pumpswap.oldestBlockTime * 1_000)} ${coverage.pool ?? "-"}\n`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
args.emit(
|
|
327
|
+
`Rows: ${rows.length}${Number.isFinite(limit) ? ` (limit ${limit})` : ""}` +
|
|
328
|
+
`${minSol > 0 ? ` min=${minSol} SOL` : ""}` +
|
|
329
|
+
`${owner ? ` owner=${ownerLabel(owner, names)}` : ""}\n\n`,
|
|
330
|
+
);
|
|
331
|
+
rows.forEach((row, index) =>
|
|
332
|
+
args.emit(`${tradeLine(row, index + 1, names)}\n`),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function ownerSummaryLine(
|
|
337
|
+
row: ReturnType<typeof analyzeTokenHistory>["owners"][number],
|
|
338
|
+
names: Map<string, string>,
|
|
339
|
+
): string {
|
|
340
|
+
return (
|
|
341
|
+
`${ownerLabel(row.owner, names).padEnd(14)} ` +
|
|
342
|
+
`buy=${fmtNumber(row.buySol)} sell=${fmtNumber(row.sellSol)} ` +
|
|
343
|
+
`net=${fmtNumber(row.netSpentSol)} SOL trades=${row.trades}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function pnlLine(
|
|
348
|
+
row: ReturnType<
|
|
349
|
+
typeof analyzeTokenHistoryForensicsFromStore
|
|
350
|
+
>["ownedPnl"][number],
|
|
351
|
+
names: Map<string, string>,
|
|
352
|
+
): string {
|
|
353
|
+
const status = row.costBasisComplete ? "" : " BASIS?";
|
|
354
|
+
const roi =
|
|
355
|
+
row.roiPct == null
|
|
356
|
+
? "-"
|
|
357
|
+
: `${row.roiPct >= 0 ? "+" : ""}${row.roiPct.toFixed(1)}%`;
|
|
358
|
+
return (
|
|
359
|
+
`${ownerLabel(row.owner, names).padEnd(14)} ` +
|
|
360
|
+
`real=${signedSol(row.realizedPnlSol).padStart(10)} ` +
|
|
361
|
+
`unreal=${signedSol(row.unrealizedPnlSol).padStart(10)} ` +
|
|
362
|
+
`total=${signedSol(row.totalPnlSol).padStart(10)} ` +
|
|
363
|
+
`costs=${fmtNumber(row.recordedExecutionCostsSol).padStart(9)} ` +
|
|
364
|
+
`net=${signedSol(row.netPnlAfterRecordedCostsSol).padStart(10)} SOL ` +
|
|
365
|
+
`roi=${roi}${status}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function runAnalyze(args: {
|
|
370
|
+
slrd: Solard;
|
|
371
|
+
mint: string;
|
|
372
|
+
flags: TokenHistoryCliFlags;
|
|
373
|
+
emit: Emit;
|
|
374
|
+
}): void {
|
|
375
|
+
const names = aliases(args.slrd);
|
|
376
|
+
const owned = [...names.keys()];
|
|
377
|
+
const top = Math.max(
|
|
378
|
+
1,
|
|
379
|
+
Math.min(100, Math.trunc(numberFlag(args.flags, "top", 10) ?? 10)),
|
|
380
|
+
);
|
|
381
|
+
const first = Math.max(
|
|
382
|
+
1,
|
|
383
|
+
Math.min(100, Math.trunc(numberFlag(args.flags, "first", 20) ?? 20)),
|
|
384
|
+
);
|
|
385
|
+
const analysis = analyzeTokenHistory(args.mint, { ownedWallets: owned });
|
|
386
|
+
const forensics = analyzeTokenHistoryForensicsFromStore(args.mint, {
|
|
387
|
+
ownedWallets: owned,
|
|
388
|
+
firstBuyerLimit: first,
|
|
389
|
+
topLimit: top,
|
|
390
|
+
});
|
|
391
|
+
const trades = loadTokenHistoryTrades(args.mint);
|
|
392
|
+
if (args.flags.has("json")) {
|
|
393
|
+
args.emit(`${json({ analysis, forensics })}\n`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const topBuyers = [...analysis.owners]
|
|
398
|
+
.filter((row) => row.buySol > 0)
|
|
399
|
+
.sort((a, b) => b.buySol - a.buySol)
|
|
400
|
+
.slice(0, top);
|
|
401
|
+
const largest = [...trades]
|
|
402
|
+
.sort((a, b) => Math.abs(b.solDeltaUi) - Math.abs(a.solDeltaUi))
|
|
403
|
+
.slice(0, top);
|
|
404
|
+
|
|
405
|
+
args.emit(
|
|
406
|
+
[
|
|
407
|
+
"TOKEN HISTORICAL ANALYSIS",
|
|
408
|
+
`Mint: ${analysis.mint}`,
|
|
409
|
+
`Coverage: ${coverageText(analysis.coverage)}`,
|
|
410
|
+
`Creation: ${fmtDate(analysis.coverage?.creationAtMs ?? null)}`,
|
|
411
|
+
`Curve: ${analysis.coverage?.bondingCurve ?? "-"}`,
|
|
412
|
+
`PumpSwap pool: ${analysis.coverage?.pool ?? "-"}`,
|
|
413
|
+
`Period: ${fmtDate(analysis.firstTradeAtMs)} -> ${fmtDate(analysis.lastTradeAtMs)}`,
|
|
414
|
+
"",
|
|
415
|
+
`Trades: ${analysis.trades} (${analysis.buys} buys / ${analysis.sells} sells)`,
|
|
416
|
+
`Unique traders: ${analysis.uniqueTraders}`,
|
|
417
|
+
`Buy volume: ${fmtNumber(analysis.buySol)} SOL`,
|
|
418
|
+
`Sell volume: ${fmtNumber(analysis.sellSol)} SOL`,
|
|
419
|
+
`Net market flow: ${fmtNumber(analysis.netInflowSol)} SOL`,
|
|
420
|
+
`Round trips: ${analysis.roundTripTraders} traders`,
|
|
421
|
+
`ATH price: ${analysis.athPriceSol == null ? "-" : fmtNumber(analysis.athPriceSol, 9)} SOL/token`,
|
|
422
|
+
`ATL price: ${analysis.atlPriceSol == null ? "-" : fmtNumber(analysis.atlPriceSol, 9)} SOL/token`,
|
|
423
|
+
`ATH mcap: ${analysis.athMarketCapSol == null ? "-" : `${fmtNumber(analysis.athMarketCapSol)} SOL`}`,
|
|
424
|
+
`ATL mcap: ${analysis.atlMarketCapSol == null ? "-" : `${fmtNumber(analysis.atlMarketCapSol)} SOL`}`,
|
|
425
|
+
`Mark price: ${forensics.markPriceSol == null ? "-" : `${fmtNumber(forensics.markPriceSol, 9)} SOL/token`}`,
|
|
426
|
+
].join("\n") + "\n",
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
args.emit("\nFIRST BUYERS / ON-CHAIN ORDER\n");
|
|
430
|
+
args.emit(
|
|
431
|
+
" rank uniq who ours createΔ firstΔ slotΔ SOL slot\n",
|
|
432
|
+
);
|
|
433
|
+
for (const row of forensics.firstBuyers) {
|
|
434
|
+
args.emit(
|
|
435
|
+
`${String(row.buyRank).padStart(5)} ` +
|
|
436
|
+
`${String(row.uniqueBuyerRank).padStart(4)} ` +
|
|
437
|
+
`${ownerLabel(row.trade.owner, names).padEnd(15)} ` +
|
|
438
|
+
`${(row.owned ? "YES" : "-").padEnd(5)} ` +
|
|
439
|
+
`${fmtDelta(row.creationDeltaMs).padStart(9)} ` +
|
|
440
|
+
`${fmtDelta(row.firstBuyDeltaMs).padStart(8)} ` +
|
|
441
|
+
`${String(row.slotDeltaFromFirstBuy ?? "-").padStart(6)} ` +
|
|
442
|
+
`${fmtNumber(Math.abs(row.trade.solDeltaUi)).padStart(8)} ` +
|
|
443
|
+
`${row.trade.slot}\n`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
args.emit("\nOUR FIRST BUY / SNIPE POSITION\n");
|
|
448
|
+
if (!forensics.ownedEntries.length) {
|
|
449
|
+
args.emit("No stored wallet has a recorded buy in this tape.\n");
|
|
450
|
+
} else {
|
|
451
|
+
for (const row of forensics.ownedEntries) {
|
|
452
|
+
args.emit(
|
|
453
|
+
`${ownerLabel(row.owner, names).padEnd(14)} ` +
|
|
454
|
+
`rank=${row.buyRank} unique=${row.uniqueBuyerRank ?? "-"} ` +
|
|
455
|
+
`createΔ=${fmtDelta(row.creationDeltaMs)} firstΔ=${fmtDelta(row.firstBuyDeltaMs)} ` +
|
|
456
|
+
`slotΔ=${row.slotDeltaFromFirstBuy ?? "-"} ` +
|
|
457
|
+
`external-ahead=${row.externalBuysAhead}/${row.externalUniqueBuyersAhead} ` +
|
|
458
|
+
`(${fmtNumber(row.externalBuySolAhead)} SOL) ` +
|
|
459
|
+
`same-sec-ahead=${row.sameTimestampBuysAhead} same-slot-ahead=${row.sameSlotBuysAhead}\n`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
args.emit(
|
|
463
|
+
"Note: blockTime is second-resolution. Same-second ordering is reliable from history order, but not millisecond latency.\n",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
args.emit("\nOUR P/L TOTAL\n");
|
|
468
|
+
args.emit(
|
|
469
|
+
`wallets=${forensics.ownedTotal.wallets} ` +
|
|
470
|
+
`buy=${fmtNumber(forensics.ownedTotal.buySol)} sell=${fmtNumber(forensics.ownedTotal.sellSol)} ` +
|
|
471
|
+
`realized=${signedSol(forensics.ownedTotal.realizedPnlSol)} ` +
|
|
472
|
+
`unrealized=${signedSol(forensics.ownedTotal.unrealizedPnlSol)} ` +
|
|
473
|
+
`total=${signedSol(forensics.ownedTotal.totalPnlSol)} ` +
|
|
474
|
+
`recorded-costs=${fmtNumber(forensics.ownedTotal.recordedExecutionCostsSol)} ` +
|
|
475
|
+
`net=${signedSol(forensics.ownedTotal.netPnlAfterRecordedCostsSol)} SOL\n`,
|
|
476
|
+
);
|
|
477
|
+
if (forensics.ownedTotal.incompleteWallets) {
|
|
478
|
+
args.emit(
|
|
479
|
+
`WARNING: ${forensics.ownedTotal.incompleteWallets} stored wallet(s) have incomplete on-chain buy cost basis; their sells may include transferred-in tokens.\n`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
args.emit("\nOUR P/L BY WALLET (FIFO)\n");
|
|
484
|
+
if (!forensics.ownedPnl.length) args.emit("-\n");
|
|
485
|
+
for (const row of forensics.ownedPnl) args.emit(`${pnlLine(row, names)}\n`);
|
|
486
|
+
|
|
487
|
+
args.emit("\nTOP REALIZED WINNERS (complete recorded buy basis)\n");
|
|
488
|
+
for (const row of forensics.topRealizedWinners)
|
|
489
|
+
args.emit(`${pnlLine(row, names)}\n`);
|
|
490
|
+
|
|
491
|
+
args.emit(
|
|
492
|
+
"\nTOP MARKED WINNERS (realized + remaining inventory @ last exact price)\n",
|
|
493
|
+
);
|
|
494
|
+
for (const row of forensics.topTotalWinners)
|
|
495
|
+
args.emit(`${pnlLine(row, names)}\n`);
|
|
496
|
+
|
|
497
|
+
args.emit("\nTOP MARKED LOSERS\n");
|
|
498
|
+
for (const row of forensics.topTotalLosers)
|
|
499
|
+
args.emit(`${pnlLine(row, names)}\n`);
|
|
500
|
+
|
|
501
|
+
args.emit("\nOUR REALIZED P/L BY PERIOD FROM CREATION\n");
|
|
502
|
+
args.emit(
|
|
503
|
+
" period mkt-buy mkt-sell our-buy our-sell realized costs net\n",
|
|
504
|
+
);
|
|
505
|
+
for (const row of forensics.periods) {
|
|
506
|
+
if (row.trades === 0 && row.ownedTrades === 0) continue;
|
|
507
|
+
args.emit(
|
|
508
|
+
`${row.label.padEnd(10)} ` +
|
|
509
|
+
`${fmtNumber(row.buySol).padStart(8)} ` +
|
|
510
|
+
`${fmtNumber(row.sellSol).padStart(9)} ` +
|
|
511
|
+
`${fmtNumber(row.ownedBuySol).padStart(9)} ` +
|
|
512
|
+
`${fmtNumber(row.ownedSellSol).padStart(9)} ` +
|
|
513
|
+
`${signedSol(row.ownedRealizedPnlSol).padStart(9)} ` +
|
|
514
|
+
`${fmtNumber(row.ownedRecordedExecutionCostsSol).padStart(8)} ` +
|
|
515
|
+
`${signedSol(row.ownedNetRealizedAfterRecordedCostsSol).padStart(9)}\n`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
args.emit("\nTOP BUYERS BY SOL\n");
|
|
520
|
+
for (const row of topBuyers) args.emit(`${ownerSummaryLine(row, names)}\n`);
|
|
521
|
+
args.emit("\nLARGEST TRADES\n");
|
|
522
|
+
largest.forEach((row, index) =>
|
|
523
|
+
args.emit(`${tradeLine(row, index + 1, names)}\n`),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function runTokenHistoryCommand(args: {
|
|
528
|
+
slrd: Solard;
|
|
529
|
+
action: string;
|
|
530
|
+
values: string[];
|
|
531
|
+
flags: TokenHistoryCliFlags;
|
|
532
|
+
emit: Emit;
|
|
533
|
+
}): Promise<void> {
|
|
534
|
+
const mint = args.values[0]?.trim();
|
|
535
|
+
if (!mint) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Usage: slrd token ${args.action} <CA> ${
|
|
538
|
+
args.action === "backfill"
|
|
539
|
+
? "[--replace] [--confirmed] [--json]"
|
|
540
|
+
: args.action === "trades"
|
|
541
|
+
? "[--from-start] [--min-sol N] [--owner wallet] [--json]"
|
|
542
|
+
: "[--top N] [--first N] [--json]"
|
|
543
|
+
}`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (args.action === "backfill") {
|
|
547
|
+
await runBackfill({ ...args, mint });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (args.action === "trades") {
|
|
551
|
+
runTrades({ ...args, mint });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (args.action === "analyze") {
|
|
555
|
+
runAnalyze({ ...args, mint });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
throw new Error("Usage: slrd token <backfill|trades|analyze> <CA>");
|
|
559
|
+
}
|