@exagent/agent 0.1.38 → 0.1.39
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/chunk-3AJR5TQF.mjs +6499 -0
- package/dist/chunk-6Y4PBQ2U.mjs +6806 -0
- package/dist/chunk-EPXZ6MEW.mjs +6959 -0
- package/dist/chunk-KQUNPLRE.mjs +6959 -0
- package/dist/chunk-LDTWZMEI.mjs +6818 -0
- package/dist/cli.js +4123 -2045
- package/dist/cli.mjs +1086 -21
- package/dist/index.d.mts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +898 -93
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,19 +1,823 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
AgentRuntime,
|
|
4
|
+
FileStore,
|
|
5
|
+
NATIVE_ETH,
|
|
6
|
+
PaperExecutor,
|
|
7
|
+
RiskManager,
|
|
8
|
+
SimulatedPortfolio,
|
|
9
|
+
__esm,
|
|
10
|
+
__export,
|
|
11
|
+
__toCommonJS,
|
|
12
|
+
createLLMAdapter,
|
|
4
13
|
encryptEnvFile,
|
|
14
|
+
formatSessionReport,
|
|
5
15
|
getAllStrategyTemplates,
|
|
16
|
+
getTokenDecimals,
|
|
17
|
+
init_adapter,
|
|
18
|
+
init_executor,
|
|
19
|
+
init_loader,
|
|
20
|
+
init_portfolio,
|
|
21
|
+
init_results,
|
|
22
|
+
init_risk,
|
|
23
|
+
init_store,
|
|
24
|
+
init_trading,
|
|
6
25
|
loadConfig,
|
|
7
26
|
loadSecureEnv,
|
|
27
|
+
loadStrategy,
|
|
28
|
+
portfolio_exports,
|
|
29
|
+
results_exports,
|
|
30
|
+
saveSessionResult,
|
|
8
31
|
validateConfig
|
|
9
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-EPXZ6MEW.mjs";
|
|
33
|
+
|
|
34
|
+
// src/backtest/data-loader.ts
|
|
35
|
+
import * as fs from "fs";
|
|
36
|
+
import * as path from "path";
|
|
37
|
+
var WETH_BASE, STABLECOINS, KNOWN_POOLS, HistoricalDataLoader;
|
|
38
|
+
var init_data_loader = __esm({
|
|
39
|
+
"src/backtest/data-loader.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
init_trading();
|
|
42
|
+
WETH_BASE = "0x4200000000000000000000000000000000000006";
|
|
43
|
+
STABLECOINS = /* @__PURE__ */ new Set([
|
|
44
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
45
|
+
// USDC
|
|
46
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
|
|
47
|
+
// USDbC
|
|
48
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
|
|
49
|
+
// DAI
|
|
50
|
+
"0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
|
|
51
|
+
// USDT
|
|
52
|
+
"0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42",
|
|
53
|
+
// EURC
|
|
54
|
+
"0x417ac0e078398c154edacc20b41affd5af7f93e1"
|
|
55
|
+
// crvUSD
|
|
56
|
+
]);
|
|
57
|
+
KNOWN_POOLS = {
|
|
58
|
+
// WETH / USDC pools
|
|
59
|
+
[WETH_BASE]: "0xd0b53d9277642d899df5c87a3966a349a798f224",
|
|
60
|
+
// AERO / WETH
|
|
61
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "0x7f670f78b17dec44c5b03398e1d13ff0e19b5f1e",
|
|
62
|
+
// VIRTUAL / WETH
|
|
63
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "0x34a1e3db82f669f8cf5bce07f2f1c09c14f1b3d4",
|
|
64
|
+
// BRETT / WETH
|
|
65
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "0xba3f945812a83471d709bce9c3ca699a19fb46f7",
|
|
66
|
+
// cbETH / WETH
|
|
67
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "0x44bc50d92990e0b15c0c300bfef4e07c073f59a0",
|
|
68
|
+
// DEGEN / WETH
|
|
69
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "0xc9034c3e7f58003e6ae0c8438e7c8f4598d5acaa",
|
|
70
|
+
// TOSHI / WETH
|
|
71
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "0xb1afe803fa26b0d4196d4e2c9da0b3baa8e2a54e",
|
|
72
|
+
// HIGHER / WETH
|
|
73
|
+
"0x0578d8a44db98b23bf096a382e016e29a5ce0ffe": "0x1fc1f3a29b82d75b2e0e8f48e6b88d0ed6e5e740",
|
|
74
|
+
// WELL / WETH
|
|
75
|
+
"0xa88594d404727625a9437c3f886c7643be7dbffc": "0x89dc31c0f71a09e6a8c15e0d0e95853e6b1d0c88",
|
|
76
|
+
// MOG / WETH
|
|
77
|
+
"0x2da56acb9ea78330f947bd57c54119debda7af71": "0x5c88b527b4bc4e52e2f7e21f5a26cbb4d4e1032c",
|
|
78
|
+
// MORPHO / WETH
|
|
79
|
+
"0xbaa5cc21fd487b8fcc2f632f3f4e8d37262987a5": "0xd4e5d3f43da7a2db7c01b08e7b33b0c1e9e7f9a5",
|
|
80
|
+
// KEYCAT / WETH
|
|
81
|
+
"0x9a26f5433671751c3276a065f57e5a02d2817973": "0x2e4784446a0a06df3d1a69afb54b2956b5bc0098"
|
|
82
|
+
};
|
|
83
|
+
HistoricalDataLoader = class {
|
|
84
|
+
cacheDir;
|
|
85
|
+
network;
|
|
86
|
+
rateLimitRpm;
|
|
87
|
+
requestTimestamps = [];
|
|
88
|
+
candles = /* @__PURE__ */ new Map();
|
|
89
|
+
// token -> candles
|
|
90
|
+
poolMappings = /* @__PURE__ */ new Map();
|
|
91
|
+
// token -> pool
|
|
92
|
+
timestamps = [];
|
|
93
|
+
constructor(options) {
|
|
94
|
+
this.cacheDir = options?.cacheDir || path.join(process.cwd(), "data", "backtest", "price-cache");
|
|
95
|
+
this.network = options?.network || "base";
|
|
96
|
+
this.rateLimitRpm = options?.rateLimitRpm || 28;
|
|
97
|
+
this.loadPoolMappings();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Prefetch OHLCV data for all tokens in the given time range.
|
|
101
|
+
* Downloads and caches data upfront so buildMarketData() is instant.
|
|
102
|
+
*/
|
|
103
|
+
async prefetch(tokens, startTime, endTime, interval, onProgress) {
|
|
104
|
+
const timeframe = interval === "daily" ? "day" : "hour";
|
|
105
|
+
const normalizedTokens = tokens.map((t) => t.toLowerCase());
|
|
106
|
+
for (let i = 0; i < normalizedTokens.length; i++) {
|
|
107
|
+
const token = normalizedTokens[i];
|
|
108
|
+
if (STABLECOINS.has(token)) {
|
|
109
|
+
onProgress?.({ token, current: i + 1, total: normalizedTokens.length, status: "stablecoin" });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const lookupToken = token === NATIVE_ETH.toLowerCase() ? WETH_BASE : token;
|
|
113
|
+
const cached = this.loadCachedOHLCV(lookupToken, timeframe);
|
|
114
|
+
if (cached && this.isCacheValid(cached, startTime, endTime)) {
|
|
115
|
+
this.candles.set(token, cached.candles);
|
|
116
|
+
if (token === NATIVE_ETH.toLowerCase()) {
|
|
117
|
+
this.candles.set(WETH_BASE, cached.candles);
|
|
118
|
+
}
|
|
119
|
+
onProgress?.({ token, current: i + 1, total: normalizedTokens.length, status: "cached" });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const pool = await this.resolvePool(lookupToken);
|
|
123
|
+
if (!pool) {
|
|
124
|
+
console.warn(` [BACKTEST] No pool found for ${token} \u2014 prices will be $0`);
|
|
125
|
+
onProgress?.({ token, current: i + 1, total: normalizedTokens.length, status: "failed" });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const candles = await this.fetchOHLCV(pool, timeframe, startTime, endTime);
|
|
130
|
+
this.candles.set(token, candles);
|
|
131
|
+
if (token === NATIVE_ETH.toLowerCase()) {
|
|
132
|
+
this.candles.set(WETH_BASE, candles);
|
|
133
|
+
}
|
|
134
|
+
this.saveCachedOHLCV(lookupToken, timeframe, pool, candles);
|
|
135
|
+
onProgress?.({ token, current: i + 1, total: normalizedTokens.length, status: "fetching" });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.warn(` [BACKTEST] Failed to fetch OHLCV for ${token}: ${err instanceof Error ? err.message : err}`);
|
|
138
|
+
onProgress?.({ token, current: i + 1, total: normalizedTokens.length, status: "failed" });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.buildTimestamps(startTime, endTime, interval);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Build a synthetic MarketData object for a given historical timestamp.
|
|
145
|
+
* Uses the closest candle close price ≤ timestamp for each token.
|
|
146
|
+
*/
|
|
147
|
+
buildMarketData(timestamp, balances, tokens) {
|
|
148
|
+
const prices = {};
|
|
149
|
+
const volume24h = {};
|
|
150
|
+
const priceChange24h = {};
|
|
151
|
+
for (const token of tokens) {
|
|
152
|
+
const key = token.toLowerCase();
|
|
153
|
+
if (STABLECOINS.has(key)) {
|
|
154
|
+
prices[key] = 1;
|
|
155
|
+
volume24h[key] = 0;
|
|
156
|
+
priceChange24h[key] = 0;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const candles = this.candles.get(key);
|
|
160
|
+
if (!candles || candles.length === 0) {
|
|
161
|
+
prices[key] = 0;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const candle = this.findClosestCandle(candles, timestamp);
|
|
165
|
+
if (candle) {
|
|
166
|
+
prices[key] = candle.close;
|
|
167
|
+
volume24h[key] = candle.volume;
|
|
168
|
+
priceChange24h[key] = candle.open > 0 ? (candle.close - candle.open) / candle.open * 100 : 0;
|
|
169
|
+
} else {
|
|
170
|
+
prices[key] = 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const nativeKey = NATIVE_ETH.toLowerCase();
|
|
174
|
+
if (!prices[nativeKey] && prices[WETH_BASE]) {
|
|
175
|
+
prices[nativeKey] = prices[WETH_BASE];
|
|
176
|
+
}
|
|
177
|
+
if (prices[nativeKey] && !prices[WETH_BASE]) {
|
|
178
|
+
prices[WETH_BASE] = prices[nativeKey];
|
|
179
|
+
}
|
|
180
|
+
let portfolioValue = 0;
|
|
181
|
+
for (const [token, balance] of Object.entries(balances)) {
|
|
182
|
+
const price = prices[token.toLowerCase()] || 0;
|
|
183
|
+
const decimals = getTokenDecimals(token);
|
|
184
|
+
portfolioValue += Number(balance) / Math.pow(10, decimals) * price;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
timestamp,
|
|
188
|
+
prices,
|
|
189
|
+
balances,
|
|
190
|
+
portfolioValue,
|
|
191
|
+
volume24h,
|
|
192
|
+
priceChange24h,
|
|
193
|
+
network: { chainId: 8453 }
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get all timestamps for the backtest loop.
|
|
198
|
+
* Returns sorted array of timestamps matching the candle interval.
|
|
199
|
+
*/
|
|
200
|
+
getTimestamps() {
|
|
201
|
+
return [...this.timestamps];
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Clear all cached data.
|
|
205
|
+
*/
|
|
206
|
+
clearCache() {
|
|
207
|
+
if (fs.existsSync(this.cacheDir)) {
|
|
208
|
+
fs.rmSync(this.cacheDir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
this.candles.clear();
|
|
211
|
+
this.poolMappings.clear();
|
|
212
|
+
this.timestamps = [];
|
|
213
|
+
}
|
|
214
|
+
// --- Private: Pool Resolution ---
|
|
215
|
+
async resolvePool(token) {
|
|
216
|
+
const key = token.toLowerCase();
|
|
217
|
+
if (KNOWN_POOLS[key]) {
|
|
218
|
+
return KNOWN_POOLS[key];
|
|
219
|
+
}
|
|
220
|
+
if (this.poolMappings.has(key)) {
|
|
221
|
+
return this.poolMappings.get(key);
|
|
222
|
+
}
|
|
223
|
+
await this.rateLimit();
|
|
224
|
+
try {
|
|
225
|
+
const url = `https://api.geckoterminal.com/api/v2/networks/${this.network}/tokens/${key}/pools?page=1`;
|
|
226
|
+
const res = await fetch(url, {
|
|
227
|
+
headers: { "Accept": "application/json" }
|
|
228
|
+
});
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
if (res.status === 429) {
|
|
231
|
+
await this.exponentialBackoff(1);
|
|
232
|
+
return this.resolvePool(token);
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
const pools = data?.data;
|
|
238
|
+
if (!pools || pools.length === 0) return null;
|
|
239
|
+
const pool = pools[0]?.id;
|
|
240
|
+
if (!pool) return null;
|
|
241
|
+
const poolAddress = pool.includes("_") ? pool.split("_")[1] : pool;
|
|
242
|
+
this.poolMappings.set(key, poolAddress);
|
|
243
|
+
this.savePoolMappings();
|
|
244
|
+
return poolAddress;
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.warn(` [BACKTEST] Pool lookup failed for ${token}: ${err instanceof Error ? err.message : err}`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// --- Private: OHLCV Fetching ---
|
|
251
|
+
async fetchOHLCV(pool, timeframe, startTime, endTime) {
|
|
252
|
+
const allCandles = [];
|
|
253
|
+
let beforeTimestamp = Math.floor(endTime / 1e3);
|
|
254
|
+
while (true) {
|
|
255
|
+
await this.rateLimit();
|
|
256
|
+
const url = `https://api.geckoterminal.com/api/v2/networks/${this.network}/pools/${pool}/ohlcv/${timeframe}?aggregate=1&before_timestamp=${beforeTimestamp}&limit=1000¤cy=usd`;
|
|
257
|
+
let res;
|
|
258
|
+
try {
|
|
259
|
+
res = await fetch(url, {
|
|
260
|
+
headers: { "Accept": "application/json" }
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
await this.exponentialBackoff(1);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (res.status === 429) {
|
|
267
|
+
await this.exponentialBackoff(1);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (!res.ok) {
|
|
271
|
+
throw new Error(`GeckoTerminal OHLCV fetch failed: ${res.status}`);
|
|
272
|
+
}
|
|
273
|
+
const data = await res.json();
|
|
274
|
+
const ohlcvList = data?.data?.attributes?.ohlcv_list;
|
|
275
|
+
if (!ohlcvList || ohlcvList.length === 0) break;
|
|
276
|
+
for (const candle of ohlcvList) {
|
|
277
|
+
const ts = candle[0];
|
|
278
|
+
const tsMs = ts * 1e3;
|
|
279
|
+
if (tsMs < startTime) continue;
|
|
280
|
+
if (tsMs > endTime) continue;
|
|
281
|
+
allCandles.push({
|
|
282
|
+
timestamp: ts,
|
|
283
|
+
open: candle[1],
|
|
284
|
+
high: candle[2],
|
|
285
|
+
low: candle[3],
|
|
286
|
+
close: candle[4],
|
|
287
|
+
volume: candle[5]
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const oldestTimestamp = ohlcvList[ohlcvList.length - 1][0];
|
|
291
|
+
if (oldestTimestamp * 1e3 <= startTime) break;
|
|
292
|
+
beforeTimestamp = oldestTimestamp - 1;
|
|
293
|
+
if (ohlcvList.length < 100) break;
|
|
294
|
+
}
|
|
295
|
+
allCandles.sort((a, b) => a.timestamp - b.timestamp);
|
|
296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
297
|
+
const unique = allCandles.filter((c) => {
|
|
298
|
+
if (seen.has(c.timestamp)) return false;
|
|
299
|
+
seen.add(c.timestamp);
|
|
300
|
+
return true;
|
|
301
|
+
});
|
|
302
|
+
return unique;
|
|
303
|
+
}
|
|
304
|
+
// --- Private: Timestamp Building ---
|
|
305
|
+
buildTimestamps(startTime, endTime, interval) {
|
|
306
|
+
const timestampSet = /* @__PURE__ */ new Set();
|
|
307
|
+
for (const candles of this.candles.values()) {
|
|
308
|
+
for (const candle of candles) {
|
|
309
|
+
const tsMs = candle.timestamp * 1e3;
|
|
310
|
+
if (tsMs >= startTime && tsMs <= endTime) {
|
|
311
|
+
timestampSet.add(tsMs);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
this.timestamps = Array.from(timestampSet).sort((a, b) => a - b);
|
|
316
|
+
if (this.timestamps.length === 0) {
|
|
317
|
+
const step = interval === "daily" ? 864e5 : 36e5;
|
|
318
|
+
let ts = startTime;
|
|
319
|
+
while (ts <= endTime) {
|
|
320
|
+
this.timestamps.push(ts);
|
|
321
|
+
ts += step;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// --- Private: Candle Lookup ---
|
|
326
|
+
findClosestCandle(candles, timestampMs) {
|
|
327
|
+
const targetSec = Math.floor(timestampMs / 1e3);
|
|
328
|
+
let lo = 0;
|
|
329
|
+
let hi = candles.length - 1;
|
|
330
|
+
let best = null;
|
|
331
|
+
while (lo <= hi) {
|
|
332
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
333
|
+
if (candles[mid].timestamp <= targetSec) {
|
|
334
|
+
best = candles[mid];
|
|
335
|
+
lo = mid + 1;
|
|
336
|
+
} else {
|
|
337
|
+
hi = mid - 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return best;
|
|
341
|
+
}
|
|
342
|
+
// --- Private: Rate Limiting ---
|
|
343
|
+
async rateLimit() {
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
const windowMs = 6e4;
|
|
346
|
+
this.requestTimestamps = this.requestTimestamps.filter((t) => now - t < windowMs);
|
|
347
|
+
if (this.requestTimestamps.length >= this.rateLimitRpm) {
|
|
348
|
+
const oldest = this.requestTimestamps[0];
|
|
349
|
+
const waitMs = windowMs - (now - oldest) + 100;
|
|
350
|
+
if (waitMs > 0) {
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
this.requestTimestamps.push(Date.now());
|
|
355
|
+
}
|
|
356
|
+
async exponentialBackoff(attempt) {
|
|
357
|
+
const maxAttempts = 3;
|
|
358
|
+
if (attempt > maxAttempts) {
|
|
359
|
+
throw new Error("Max retries exceeded for GeckoTerminal API");
|
|
360
|
+
}
|
|
361
|
+
const waitMs = Math.pow(2, attempt) * 1e3 + Math.random() * 1e3;
|
|
362
|
+
console.warn(` [BACKTEST] Rate limited, waiting ${(waitMs / 1e3).toFixed(1)}s (attempt ${attempt}/${maxAttempts})`);
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
364
|
+
}
|
|
365
|
+
// --- Private: Disk Cache ---
|
|
366
|
+
getCacheDir() {
|
|
367
|
+
const dir = path.join(this.cacheDir, this.network);
|
|
368
|
+
if (!fs.existsSync(dir)) {
|
|
369
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
return dir;
|
|
372
|
+
}
|
|
373
|
+
loadCachedOHLCV(token, timeframe) {
|
|
374
|
+
const filePath = path.join(this.getCacheDir(), `${token}-${timeframe}.json`);
|
|
375
|
+
if (!fs.existsSync(filePath)) return null;
|
|
376
|
+
try {
|
|
377
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
378
|
+
return raw;
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
saveCachedOHLCV(token, timeframe, pool, candles) {
|
|
384
|
+
const filePath = path.join(this.getCacheDir(), `${token}-${timeframe}.json`);
|
|
385
|
+
const data = {
|
|
386
|
+
pool,
|
|
387
|
+
token,
|
|
388
|
+
timeframe,
|
|
389
|
+
fetchedAt: Date.now(),
|
|
390
|
+
candles
|
|
391
|
+
};
|
|
392
|
+
fs.writeFileSync(filePath, JSON.stringify(data));
|
|
393
|
+
}
|
|
394
|
+
isCacheValid(cached, startTime, endTime) {
|
|
395
|
+
if (!cached.candles.length) return false;
|
|
396
|
+
const firstCandleMs = cached.candles[0].timestamp * 1e3;
|
|
397
|
+
const lastCandleMs = cached.candles[cached.candles.length - 1].timestamp * 1e3;
|
|
398
|
+
const tolerance = 864e5;
|
|
399
|
+
return firstCandleMs <= startTime + tolerance && lastCandleMs >= endTime - tolerance;
|
|
400
|
+
}
|
|
401
|
+
loadPoolMappings() {
|
|
402
|
+
const filePath = path.join(this.getCacheDir(), "pool-mappings.json");
|
|
403
|
+
if (!fs.existsSync(filePath)) return;
|
|
404
|
+
try {
|
|
405
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
406
|
+
for (const mapping of raw) {
|
|
407
|
+
this.poolMappings.set(mapping.token.toLowerCase(), mapping.pool);
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
savePoolMappings() {
|
|
413
|
+
const filePath = path.join(this.getCacheDir(), "pool-mappings.json");
|
|
414
|
+
const mappings = [];
|
|
415
|
+
for (const [token, pool] of this.poolMappings) {
|
|
416
|
+
mappings.push({ token, pool, fetchedAt: Date.now() });
|
|
417
|
+
}
|
|
418
|
+
fs.writeFileSync(filePath, JSON.stringify(mappings, null, 2));
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// src/backtest/llm-cache.ts
|
|
425
|
+
import * as fs2 from "fs";
|
|
426
|
+
import * as path2 from "path";
|
|
427
|
+
import * as crypto from "crypto";
|
|
428
|
+
var COST_PER_MILLION_INPUT, LLMCacheAdapter;
|
|
429
|
+
var init_llm_cache = __esm({
|
|
430
|
+
"src/backtest/llm-cache.ts"() {
|
|
431
|
+
"use strict";
|
|
432
|
+
COST_PER_MILLION_INPUT = {
|
|
433
|
+
"gpt-4": 30,
|
|
434
|
+
"gpt-4o": 2.5,
|
|
435
|
+
"gpt-4o-mini": 0.15,
|
|
436
|
+
"gpt-3.5": 0.5,
|
|
437
|
+
"claude-3-opus": 15,
|
|
438
|
+
"claude-3.5-sonnet": 3,
|
|
439
|
+
"claude-3-haiku": 0.25,
|
|
440
|
+
"deepseek-chat": 0.14,
|
|
441
|
+
"deepseek-reasoner": 0.55,
|
|
442
|
+
"gemini-pro": 0.5,
|
|
443
|
+
"gemini-flash": 0.075,
|
|
444
|
+
"llama": 0,
|
|
445
|
+
// local
|
|
446
|
+
"mistral": 0.25,
|
|
447
|
+
"mixtral": 0.6,
|
|
448
|
+
"groq": 0.05,
|
|
449
|
+
"together": 0.2,
|
|
450
|
+
"default": 1
|
|
451
|
+
};
|
|
452
|
+
LLMCacheAdapter = class {
|
|
453
|
+
inner;
|
|
454
|
+
cacheDir;
|
|
455
|
+
noCache;
|
|
456
|
+
stats;
|
|
457
|
+
constructor(inner, options) {
|
|
458
|
+
this.inner = inner;
|
|
459
|
+
this.cacheDir = options.cacheDir;
|
|
460
|
+
this.noCache = options.noCache ?? false;
|
|
461
|
+
this.stats = {
|
|
462
|
+
hits: 0,
|
|
463
|
+
misses: 0,
|
|
464
|
+
totalPromptTokens: 0,
|
|
465
|
+
totalCompletionTokens: 0,
|
|
466
|
+
estimatedCostUSD: 0
|
|
467
|
+
};
|
|
468
|
+
if (!this.noCache && !fs2.existsSync(this.cacheDir)) {
|
|
469
|
+
fs2.mkdirSync(this.cacheDir, { recursive: true });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async chat(messages) {
|
|
473
|
+
const metadata = this.inner.getMetadata();
|
|
474
|
+
const cacheKey = this.buildCacheKey(metadata.model, messages);
|
|
475
|
+
if (!this.noCache) {
|
|
476
|
+
const cached = this.readCache(cacheKey);
|
|
477
|
+
if (cached) {
|
|
478
|
+
this.stats.hits++;
|
|
479
|
+
this.trackUsage(cached.response);
|
|
480
|
+
return cached.response;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.stats.misses++;
|
|
484
|
+
const response = await this.inner.chat(messages);
|
|
485
|
+
this.trackUsage(response);
|
|
486
|
+
if (!this.noCache) {
|
|
487
|
+
this.writeCache(cacheKey, {
|
|
488
|
+
response,
|
|
489
|
+
cachedAt: Date.now(),
|
|
490
|
+
modelId: metadata.model
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return response;
|
|
494
|
+
}
|
|
495
|
+
getMetadata() {
|
|
496
|
+
return this.inner.getMetadata();
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get cache statistics for cost reporting.
|
|
500
|
+
*/
|
|
501
|
+
getStats() {
|
|
502
|
+
return { ...this.stats };
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Clear the entire cache directory.
|
|
506
|
+
*/
|
|
507
|
+
clearCache() {
|
|
508
|
+
if (fs2.existsSync(this.cacheDir)) {
|
|
509
|
+
fs2.rmSync(this.cacheDir, { recursive: true, force: true });
|
|
510
|
+
fs2.mkdirSync(this.cacheDir, { recursive: true });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// --- Private ---
|
|
514
|
+
/**
|
|
515
|
+
* Build a deterministic cache key from model + messages.
|
|
516
|
+
* SHA256 of: "model:{modelId}\n" + each "role:content\n"
|
|
517
|
+
*/
|
|
518
|
+
buildCacheKey(model, messages) {
|
|
519
|
+
const parts = [`model:${model}`];
|
|
520
|
+
for (const msg of messages) {
|
|
521
|
+
parts.push(`${msg.role}:${msg.content}`);
|
|
522
|
+
}
|
|
523
|
+
return crypto.createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Read a cached response from disk.
|
|
527
|
+
* Returns null on cache miss or corrupt file.
|
|
528
|
+
*/
|
|
529
|
+
readCache(key) {
|
|
530
|
+
const filePath = this.getCachePath(key);
|
|
531
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
532
|
+
try {
|
|
533
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
534
|
+
return JSON.parse(raw);
|
|
535
|
+
} catch {
|
|
536
|
+
try {
|
|
537
|
+
fs2.unlinkSync(filePath);
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
console.warn(` [BACKTEST] Corrupt cache file deleted: ${key.slice(0, 8)}`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Write a response to disk cache.
|
|
546
|
+
*/
|
|
547
|
+
writeCache(key, data) {
|
|
548
|
+
const filePath = this.getCachePath(key);
|
|
549
|
+
const dir = path2.dirname(filePath);
|
|
550
|
+
if (!fs2.existsSync(dir)) {
|
|
551
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
fs2.writeFileSync(filePath, JSON.stringify(data));
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.warn(` [BACKTEST] Failed to write cache: ${err instanceof Error ? err.message : err}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get the file path for a cache key.
|
|
561
|
+
* Uses first 8 chars as subdirectory for filesystem friendliness.
|
|
562
|
+
*/
|
|
563
|
+
getCachePath(key) {
|
|
564
|
+
const prefix = key.slice(0, 8);
|
|
565
|
+
return path2.join(this.cacheDir, prefix, `${key}.json`);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Track token usage and estimate costs.
|
|
569
|
+
*/
|
|
570
|
+
trackUsage(response) {
|
|
571
|
+
if (response.usage) {
|
|
572
|
+
this.stats.totalPromptTokens += response.usage.promptTokens;
|
|
573
|
+
this.stats.totalCompletionTokens += response.usage.completionTokens;
|
|
574
|
+
}
|
|
575
|
+
const model = this.inner.getMetadata().model.toLowerCase();
|
|
576
|
+
let costPerMillion = COST_PER_MILLION_INPUT["default"];
|
|
577
|
+
for (const [pattern, cost] of Object.entries(COST_PER_MILLION_INPUT)) {
|
|
578
|
+
if (model.includes(pattern)) {
|
|
579
|
+
costPerMillion = cost;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (response.usage) {
|
|
584
|
+
const inputCost = response.usage.promptTokens / 1e6 * costPerMillion;
|
|
585
|
+
const outputCost = response.usage.completionTokens / 1e6 * costPerMillion * 3;
|
|
586
|
+
this.stats.estimatedCostUSD += inputCost + outputCost;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// src/backtest/runner.ts
|
|
594
|
+
var runner_exports = {};
|
|
595
|
+
__export(runner_exports, {
|
|
596
|
+
BacktestRunner: () => BacktestRunner
|
|
597
|
+
});
|
|
598
|
+
import * as path3 from "path";
|
|
599
|
+
var RISK_UNIVERSE_MAP, BacktestRunner;
|
|
600
|
+
var init_runner = __esm({
|
|
601
|
+
"src/backtest/runner.ts"() {
|
|
602
|
+
"use strict";
|
|
603
|
+
init_store();
|
|
604
|
+
init_adapter();
|
|
605
|
+
init_risk();
|
|
606
|
+
init_loader();
|
|
607
|
+
init_executor();
|
|
608
|
+
init_portfolio();
|
|
609
|
+
init_results();
|
|
610
|
+
init_data_loader();
|
|
611
|
+
init_llm_cache();
|
|
612
|
+
RISK_UNIVERSE_MAP = {
|
|
613
|
+
core: 0,
|
|
614
|
+
established: 1,
|
|
615
|
+
derivatives: 2,
|
|
616
|
+
emerging: 3,
|
|
617
|
+
frontier: 4
|
|
618
|
+
};
|
|
619
|
+
BacktestRunner = class {
|
|
620
|
+
config;
|
|
621
|
+
constructor(config) {
|
|
622
|
+
this.config = config;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Run the backtest.
|
|
626
|
+
*
|
|
627
|
+
* Steps through historical timestamps, builds synthetic MarketData
|
|
628
|
+
* from cached OHLCV candles, calls the strategy, filters through
|
|
629
|
+
* risk manager, simulates trades, and records results.
|
|
630
|
+
*/
|
|
631
|
+
async run(onProgress) {
|
|
632
|
+
const {
|
|
633
|
+
agentConfig,
|
|
634
|
+
startTime,
|
|
635
|
+
endTime,
|
|
636
|
+
interval,
|
|
637
|
+
initialBalances,
|
|
638
|
+
tokenAddresses,
|
|
639
|
+
noLLMCache,
|
|
640
|
+
freshStart
|
|
641
|
+
} = this.config;
|
|
642
|
+
const baseDataDir = this.config.dataDir || path3.join(process.cwd(), "data", "backtest");
|
|
643
|
+
const startDate = new Date(startTime).toISOString().split("T")[0];
|
|
644
|
+
const endDate = new Date(endTime).toISOString().split("T")[0];
|
|
645
|
+
const backtestId = `backtest-${startDate}-${endDate}-${Date.now()}`;
|
|
646
|
+
const backtestDir = path3.join(baseDataDir, backtestId);
|
|
647
|
+
const store = new FileStore(path3.join(backtestDir, "store"));
|
|
648
|
+
const innerLLM = await createLLMAdapter(agentConfig.llm);
|
|
649
|
+
const llmMetadata = innerLLM.getMetadata();
|
|
650
|
+
const llmCacheDir = path3.join(backtestDir, "llm-cache");
|
|
651
|
+
const cachedLLM = new LLMCacheAdapter(innerLLM, {
|
|
652
|
+
cacheDir: llmCacheDir,
|
|
653
|
+
noCache: noLLMCache
|
|
654
|
+
});
|
|
655
|
+
const priceCache = path3.join(baseDataDir, "price-cache");
|
|
656
|
+
const dataLoader = new HistoricalDataLoader({ cacheDir: priceCache });
|
|
657
|
+
if (freshStart) {
|
|
658
|
+
dataLoader.clearCache();
|
|
659
|
+
cachedLLM.clearCache();
|
|
660
|
+
}
|
|
661
|
+
console.log("");
|
|
662
|
+
console.log(" Fetching historical price data...");
|
|
663
|
+
await dataLoader.prefetch(
|
|
664
|
+
tokenAddresses,
|
|
665
|
+
startTime,
|
|
666
|
+
endTime,
|
|
667
|
+
interval,
|
|
668
|
+
(progress) => {
|
|
669
|
+
const status = progress.status === "cached" ? "(cached)" : progress.status === "stablecoin" ? "($1)" : progress.status === "failed" ? "(FAILED)" : "";
|
|
670
|
+
process.stdout.write(`\r [${progress.current}/${progress.total}] ${progress.token.slice(0, 10)}... ${status} `);
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
674
|
+
console.log(" Price data ready.");
|
|
675
|
+
console.log("");
|
|
676
|
+
const riskUniverse = RISK_UNIVERSE_MAP[agentConfig.riskUniverse] ?? 1;
|
|
677
|
+
const riskManager = new RiskManager(agentConfig.trading, riskUniverse);
|
|
678
|
+
const dummyConfig = {
|
|
679
|
+
...agentConfig,
|
|
680
|
+
privateKey: "0x" + "0".repeat(64)
|
|
681
|
+
};
|
|
682
|
+
const executor = new PaperExecutor(dummyConfig);
|
|
683
|
+
const portfolio = new SimulatedPortfolio(
|
|
684
|
+
initialBalances,
|
|
685
|
+
path3.join(backtestDir, "portfolio"),
|
|
686
|
+
{ startedAt: startTime }
|
|
687
|
+
);
|
|
688
|
+
const strategy = await loadStrategy();
|
|
689
|
+
const agentId = typeof agentConfig.agentId === "string" ? parseInt(agentConfig.agentId, 10) || 0 : agentConfig.agentId;
|
|
690
|
+
const context = {
|
|
691
|
+
store,
|
|
692
|
+
agentId,
|
|
693
|
+
walletAddress: "0x0000000000000000000000000000000000000000",
|
|
694
|
+
positions: [],
|
|
695
|
+
tradeHistory: []
|
|
696
|
+
};
|
|
697
|
+
const timestamps = dataLoader.getTimestamps().filter((ts) => ts >= startTime && ts <= endTime);
|
|
698
|
+
if (timestamps.length === 0) {
|
|
699
|
+
throw new Error("No timestamps found in the specified range. Check your date range and price data.");
|
|
700
|
+
}
|
|
701
|
+
let totalTrades = 0;
|
|
702
|
+
for (let i = 0; i < timestamps.length; i++) {
|
|
703
|
+
const ts = timestamps[i];
|
|
704
|
+
const marketData = dataLoader.buildMarketData(
|
|
705
|
+
ts,
|
|
706
|
+
portfolio.getBalances(),
|
|
707
|
+
tokenAddresses
|
|
708
|
+
);
|
|
709
|
+
let tradesThisStep = 0;
|
|
710
|
+
try {
|
|
711
|
+
const signals = await strategy(marketData, cachedLLM, agentConfig, context);
|
|
712
|
+
const filtered = riskManager.filterSignals(signals, marketData);
|
|
713
|
+
executor.updatePrices(marketData.prices);
|
|
714
|
+
const results = await executor.executeAll(filtered);
|
|
715
|
+
for (const result of results) {
|
|
716
|
+
if (result.success && result.paperFill) {
|
|
717
|
+
const fill = {
|
|
718
|
+
...result.paperFill,
|
|
719
|
+
timestamp: ts
|
|
720
|
+
};
|
|
721
|
+
portfolio.applyFill(fill);
|
|
722
|
+
tradesThisStep++;
|
|
723
|
+
totalTrades++;
|
|
724
|
+
const pnl = fill.valueOutUSD - fill.valueInUSD;
|
|
725
|
+
riskManager.updatePnL(pnl);
|
|
726
|
+
riskManager.updateFees(fill.feeUSD);
|
|
727
|
+
const tradeRecord = {
|
|
728
|
+
timestamp: ts,
|
|
729
|
+
action: fill.signal.action,
|
|
730
|
+
tokenIn: fill.signal.tokenIn,
|
|
731
|
+
tokenOut: fill.signal.tokenOut,
|
|
732
|
+
amountIn: fill.signal.amountIn.toString(),
|
|
733
|
+
priceUSD: fill.valueInUSD,
|
|
734
|
+
txHash: `backtest-${i}-${totalTrades}`,
|
|
735
|
+
reasoning: fill.signal.reasoning,
|
|
736
|
+
success: true
|
|
737
|
+
};
|
|
738
|
+
context.tradeHistory = [tradeRecord, ...(context.tradeHistory || []).slice(0, 49)];
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.warn(` [BACKTEST] Strategy error at ${new Date(ts).toISOString()}: ${err instanceof Error ? err.message : err}`);
|
|
743
|
+
}
|
|
744
|
+
portfolio.recordEquityPoint(marketData.prices, ts);
|
|
745
|
+
context.positions = this.buildPositionsFromPortfolio(portfolio, marketData.prices);
|
|
746
|
+
onProgress?.({
|
|
747
|
+
step: i,
|
|
748
|
+
total: timestamps.length,
|
|
749
|
+
timestamp: ts,
|
|
750
|
+
date: new Date(ts).toISOString().split("T")[0],
|
|
751
|
+
portfolioValue: marketData.portfolioValue,
|
|
752
|
+
tradesThisStep,
|
|
753
|
+
totalTrades
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const summary = portfolio.getSummary(
|
|
757
|
+
dataLoader.buildMarketData(endTime, portfolio.getBalances(), tokenAddresses).prices
|
|
758
|
+
);
|
|
759
|
+
const sessionResult = saveSessionResult(
|
|
760
|
+
path3.join(baseDataDir, "sessions-backtest"),
|
|
761
|
+
agentConfig.name,
|
|
762
|
+
{ provider: llmMetadata.provider, model: llmMetadata.model },
|
|
763
|
+
summary,
|
|
764
|
+
portfolio.getEquityCurve(),
|
|
765
|
+
portfolio.getTrades(),
|
|
766
|
+
startTime,
|
|
767
|
+
{ endedAt: endTime, idPrefix: "backtest" }
|
|
768
|
+
);
|
|
769
|
+
console.log(formatSessionReport(sessionResult));
|
|
770
|
+
const llmStats = cachedLLM.getStats();
|
|
771
|
+
console.log(" LLM CACHE STATS");
|
|
772
|
+
console.log(" " + "\u2500".repeat(56));
|
|
773
|
+
console.log(` Cache hits: ${llmStats.hits}`);
|
|
774
|
+
console.log(` Cache misses: ${llmStats.misses} (real LLM calls)`);
|
|
775
|
+
console.log(` Total tokens: ${(llmStats.totalPromptTokens + llmStats.totalCompletionTokens).toLocaleString()}`);
|
|
776
|
+
console.log(` Estimated cost: $${llmStats.estimatedCostUSD.toFixed(4)}`);
|
|
777
|
+
console.log("");
|
|
778
|
+
return sessionResult;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Build a simplified TrackedPosition array from portfolio balances.
|
|
782
|
+
* This isn't as detailed as the real PositionTracker, but gives
|
|
783
|
+
* the strategy enough context to make decisions.
|
|
784
|
+
*/
|
|
785
|
+
buildPositionsFromPortfolio(portfolio, prices) {
|
|
786
|
+
const positions = [];
|
|
787
|
+
const balances = portfolio.getBalances();
|
|
788
|
+
const NATIVE_ETH2 = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
789
|
+
const WETH = "0x4200000000000000000000000000000000000006";
|
|
790
|
+
const USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
791
|
+
const baseTokens = /* @__PURE__ */ new Set([NATIVE_ETH2, WETH, USDC]);
|
|
792
|
+
for (const [token, balance] of Object.entries(balances)) {
|
|
793
|
+
if (balance <= BigInt(0)) continue;
|
|
794
|
+
if (baseTokens.has(token.toLowerCase())) continue;
|
|
795
|
+
const price = prices[token.toLowerCase()] || 0;
|
|
796
|
+
if (price <= 0) continue;
|
|
797
|
+
positions.push({
|
|
798
|
+
token: token.toLowerCase(),
|
|
799
|
+
entryPrice: price,
|
|
800
|
+
averageEntryPrice: price,
|
|
801
|
+
totalCostBasis: 0,
|
|
802
|
+
totalAmountAcquired: 0,
|
|
803
|
+
currentAmount: Number(balance),
|
|
804
|
+
entryTimestamp: Date.now(),
|
|
805
|
+
lastUpdateTimestamp: Date.now(),
|
|
806
|
+
txHashes: []
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return positions;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
});
|
|
10
814
|
|
|
11
815
|
// src/cli.ts
|
|
12
816
|
import { Command } from "commander";
|
|
13
817
|
import { config as loadEnvFile } from "dotenv";
|
|
14
818
|
import * as readline from "readline";
|
|
15
|
-
import * as
|
|
16
|
-
import * as
|
|
819
|
+
import * as fs3 from "fs";
|
|
820
|
+
import * as path4 from "path";
|
|
17
821
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
18
822
|
loadEnvFile();
|
|
19
823
|
var program = new Command();
|
|
@@ -59,13 +863,13 @@ function prompt(question, hidden = false) {
|
|
|
59
863
|
});
|
|
60
864
|
}
|
|
61
865
|
async function checkFirstRunSetup(configPath) {
|
|
62
|
-
const envPath =
|
|
866
|
+
const envPath = path4.join(path4.dirname(configPath), ".env");
|
|
63
867
|
const encPath = envPath + ".enc";
|
|
64
|
-
if (
|
|
868
|
+
if (fs3.existsSync(encPath)) {
|
|
65
869
|
return;
|
|
66
870
|
}
|
|
67
|
-
if (
|
|
68
|
-
const envContent2 =
|
|
871
|
+
if (fs3.existsSync(envPath)) {
|
|
872
|
+
const envContent2 = fs3.readFileSync(envPath, "utf-8");
|
|
69
873
|
const hasPrivateKey = envContent2.includes("EXAGENT_PRIVATE_KEY=") && !envContent2.includes("EXAGENT_PRIVATE_KEY=\n") && !envContent2.includes("EXAGENT_PRIVATE_KEY=$");
|
|
70
874
|
if (hasPrivateKey) {
|
|
71
875
|
return;
|
|
@@ -248,7 +1052,7 @@ ${rpcEnvVar}
|
|
|
248
1052
|
# LLM (${llmProvider})
|
|
249
1053
|
${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
|
|
250
1054
|
`;
|
|
251
|
-
|
|
1055
|
+
fs3.writeFileSync(envPath, envContent, { mode: 384 });
|
|
252
1056
|
console.log("");
|
|
253
1057
|
console.log("=".repeat(60));
|
|
254
1058
|
console.log(" ENCRYPT YOUR SECRETS");
|
|
@@ -306,12 +1110,12 @@ ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
|
|
|
306
1110
|
program.command("run").description("Start the trading agent").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").action(async (options) => {
|
|
307
1111
|
try {
|
|
308
1112
|
await checkFirstRunSetup(options.config);
|
|
309
|
-
const configDir =
|
|
310
|
-
options.config.startsWith("/") ? options.config :
|
|
1113
|
+
const configDir = path4.dirname(
|
|
1114
|
+
options.config.startsWith("/") ? options.config : path4.join(process.cwd(), options.config)
|
|
311
1115
|
);
|
|
312
1116
|
let passphrase = options.passphrase || process.env.EXAGENT_PASSPHRASE;
|
|
313
|
-
const encPath =
|
|
314
|
-
if (
|
|
1117
|
+
const encPath = path4.join(configDir, ".env.enc");
|
|
1118
|
+
if (fs3.existsSync(encPath) && !passphrase) {
|
|
315
1119
|
console.log("");
|
|
316
1120
|
console.log(" Encrypted config found (.env.enc)");
|
|
317
1121
|
console.log("");
|
|
@@ -355,6 +1159,139 @@ program.command("run").description("Start the trading agent").option("-c, --conf
|
|
|
355
1159
|
process.exit(1);
|
|
356
1160
|
}
|
|
357
1161
|
});
|
|
1162
|
+
program.command("paper").description("Start paper trading (simulated execution, no on-chain trades)").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").option("--initial-eth <amount>", "Override initial ETH balance (e.g., 0.5)").option("--initial-usdc <amount>", "Override initial USDC balance (e.g., 1000)").option("--fresh", "Discard any saved paper session and start fresh").action(async (options) => {
|
|
1163
|
+
try {
|
|
1164
|
+
await checkFirstRunSetup(options.config);
|
|
1165
|
+
const configDir = path4.dirname(
|
|
1166
|
+
options.config.startsWith("/") ? options.config : path4.join(process.cwd(), options.config)
|
|
1167
|
+
);
|
|
1168
|
+
let passphrase = options.passphrase || process.env.EXAGENT_PASSPHRASE;
|
|
1169
|
+
const encPath = path4.join(configDir, ".env.enc");
|
|
1170
|
+
if (fs3.existsSync(encPath) && !passphrase) {
|
|
1171
|
+
console.log("");
|
|
1172
|
+
console.log(" Encrypted config found (.env.enc)");
|
|
1173
|
+
console.log("");
|
|
1174
|
+
passphrase = await prompt(" Enter passphrase: ", true);
|
|
1175
|
+
console.log("");
|
|
1176
|
+
}
|
|
1177
|
+
const usedEncrypted = loadSecureEnv(configDir, passphrase);
|
|
1178
|
+
if (usedEncrypted) {
|
|
1179
|
+
console.log("Loaded encrypted environment (.env.enc)");
|
|
1180
|
+
}
|
|
1181
|
+
console.log("Loading configuration...");
|
|
1182
|
+
const config = loadConfig(options.config);
|
|
1183
|
+
validateConfig(config);
|
|
1184
|
+
console.log("");
|
|
1185
|
+
console.log("=".repeat(50));
|
|
1186
|
+
console.log(" EXAGENT PAPER TRADING");
|
|
1187
|
+
console.log(" Simulated execution \u2014 no on-chain trades");
|
|
1188
|
+
console.log("=".repeat(50));
|
|
1189
|
+
console.log("");
|
|
1190
|
+
console.log(` Agent: ${config.name} (ID: ${config.agentId})`);
|
|
1191
|
+
console.log(` Network: ${config.network}`);
|
|
1192
|
+
console.log(` LLM: ${config.llm.provider} (${config.llm.model || "default"})`);
|
|
1193
|
+
console.log(` Universe: ${config.riskUniverse}`);
|
|
1194
|
+
console.log("");
|
|
1195
|
+
console.log(" NOTE: This will make real LLM API calls using your");
|
|
1196
|
+
console.log(" configured provider. Each cycle costs ~$0.01-0.10.");
|
|
1197
|
+
console.log("");
|
|
1198
|
+
console.log(" Paper trading results do not guarantee future");
|
|
1199
|
+
console.log(" performance. Real trading may differ due to");
|
|
1200
|
+
console.log(" slippage, liquidity, and execution timing.");
|
|
1201
|
+
console.log("");
|
|
1202
|
+
console.log("=".repeat(50));
|
|
1203
|
+
console.log("");
|
|
1204
|
+
let paperBalances;
|
|
1205
|
+
if (options.initialEth || options.initialUsdc) {
|
|
1206
|
+
paperBalances = {};
|
|
1207
|
+
const NATIVE_ETH2 = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1208
|
+
const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
1209
|
+
if (options.initialEth) {
|
|
1210
|
+
const ethAmount = parseFloat(options.initialEth);
|
|
1211
|
+
paperBalances[NATIVE_ETH2] = BigInt(Math.floor(ethAmount * 1e18));
|
|
1212
|
+
console.log(` Initial ETH: ${ethAmount}`);
|
|
1213
|
+
}
|
|
1214
|
+
if (options.initialUsdc) {
|
|
1215
|
+
const usdcAmount = parseFloat(options.initialUsdc);
|
|
1216
|
+
paperBalances[USDC_BASE] = BigInt(Math.floor(usdcAmount * 1e6));
|
|
1217
|
+
console.log(` Initial USDC: ${usdcAmount}`);
|
|
1218
|
+
}
|
|
1219
|
+
console.log("");
|
|
1220
|
+
}
|
|
1221
|
+
if (options.fresh) {
|
|
1222
|
+
const { SimulatedPortfolio: SimulatedPortfolio2 } = (init_portfolio(), __toCommonJS(portfolio_exports));
|
|
1223
|
+
SimulatedPortfolio2.clear(path4.join(process.cwd(), "data", "paper"));
|
|
1224
|
+
console.log(" Cleared previous paper session");
|
|
1225
|
+
console.log("");
|
|
1226
|
+
}
|
|
1227
|
+
const agent = new AgentRuntime(config, {
|
|
1228
|
+
paperMode: true,
|
|
1229
|
+
paperBalances
|
|
1230
|
+
});
|
|
1231
|
+
await agent.initialize();
|
|
1232
|
+
process.on("SIGINT", () => {
|
|
1233
|
+
console.log("\nReceived SIGINT, shutting down paper trading...");
|
|
1234
|
+
agent.stop();
|
|
1235
|
+
process.exit(0);
|
|
1236
|
+
});
|
|
1237
|
+
process.on("SIGTERM", () => {
|
|
1238
|
+
console.log("\nReceived SIGTERM, shutting down paper trading...");
|
|
1239
|
+
agent.stop();
|
|
1240
|
+
process.exit(0);
|
|
1241
|
+
});
|
|
1242
|
+
await agent.run();
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
program.command("paper-results").description("View paper trading session results").option("--session <id>", "Show detailed results for a specific session").option("--json", "Output as JSON").action(async (options) => {
|
|
1249
|
+
const { loadSessionResults, loadSessionResult, formatSessionReport: formatSessionReport2, formatSessionLine } = (init_results(), __toCommonJS(results_exports));
|
|
1250
|
+
const dataDir = path4.join(process.cwd(), "data", "paper");
|
|
1251
|
+
if (options.session) {
|
|
1252
|
+
const result = loadSessionResult(dataDir, options.session);
|
|
1253
|
+
if (!result) {
|
|
1254
|
+
console.error(`Session not found: ${options.session}`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
if (options.json) {
|
|
1258
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1259
|
+
} else {
|
|
1260
|
+
console.log(formatSessionReport2(result));
|
|
1261
|
+
}
|
|
1262
|
+
} else {
|
|
1263
|
+
const results = loadSessionResults(dataDir);
|
|
1264
|
+
if (results.length === 0) {
|
|
1265
|
+
console.log("");
|
|
1266
|
+
console.log(" No paper trading sessions found.");
|
|
1267
|
+
console.log(' Run "npx @exagent/agent paper" to start a paper trading session.');
|
|
1268
|
+
console.log("");
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (options.json) {
|
|
1272
|
+
console.log(JSON.stringify(results.map((r) => ({
|
|
1273
|
+
id: r.id,
|
|
1274
|
+
agentName: r.agentName,
|
|
1275
|
+
startedAt: r.startedAt,
|
|
1276
|
+
durationMs: r.durationMs,
|
|
1277
|
+
returnPct: r.metrics.totalReturnPct,
|
|
1278
|
+
trades: r.trades.length,
|
|
1279
|
+
winRate: r.metrics.winRate
|
|
1280
|
+
})), null, 2));
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
console.log("");
|
|
1284
|
+
console.log(" PAPER TRADING SESSIONS");
|
|
1285
|
+
console.log(" " + "\u2500".repeat(56));
|
|
1286
|
+
console.log("");
|
|
1287
|
+
for (const result of results) {
|
|
1288
|
+
console.log(formatSessionLine(result));
|
|
1289
|
+
}
|
|
1290
|
+
console.log("");
|
|
1291
|
+
console.log(" Use --session <id> for detailed results");
|
|
1292
|
+
console.log("");
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
358
1295
|
program.command("templates").description("List available strategy templates").action(() => {
|
|
359
1296
|
console.log("");
|
|
360
1297
|
console.log("Available Strategy Templates:");
|
|
@@ -437,14 +1374,14 @@ program.command("api-keys").description("Show how to get API keys for each LLM p
|
|
|
437
1374
|
});
|
|
438
1375
|
program.command("encrypt").description("Encrypt .env file to .env.enc for secure storage").option("-d, --dir <path>", "Directory containing .env file", ".").option("--delete", "Delete plaintext .env after encryption", false).action(async (options) => {
|
|
439
1376
|
try {
|
|
440
|
-
const dir = options.dir.startsWith("/") ? options.dir :
|
|
441
|
-
const envPath =
|
|
442
|
-
if (!
|
|
1377
|
+
const dir = options.dir.startsWith("/") ? options.dir : path4.join(process.cwd(), options.dir);
|
|
1378
|
+
const envPath = path4.join(dir, ".env");
|
|
1379
|
+
if (!fs3.existsSync(envPath)) {
|
|
443
1380
|
console.error("No .env file found in", dir);
|
|
444
1381
|
process.exit(1);
|
|
445
1382
|
}
|
|
446
1383
|
const encPath = envPath + ".enc";
|
|
447
|
-
if (
|
|
1384
|
+
if (fs3.existsSync(encPath)) {
|
|
448
1385
|
const overwrite = await prompt(" .env.enc already exists. Overwrite? (y/n): ");
|
|
449
1386
|
if (overwrite.toLowerCase() !== "y") {
|
|
450
1387
|
console.log("Aborted.");
|
|
@@ -493,11 +1430,11 @@ program.command("encrypt").description("Encrypt .env file to .env.enc for secure
|
|
|
493
1430
|
});
|
|
494
1431
|
program.command("export-key").description("Display your trading wallet private key for backup").option("-d, --dir <path>", "Directory containing .env or .env.enc file", ".").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").action(async (options) => {
|
|
495
1432
|
try {
|
|
496
|
-
const dir = options.dir.startsWith("/") ? options.dir :
|
|
1433
|
+
const dir = options.dir.startsWith("/") ? options.dir : path4.join(process.cwd(), options.dir);
|
|
497
1434
|
let passphrase = options.passphrase || process.env.EXAGENT_PASSPHRASE;
|
|
498
|
-
const encPath =
|
|
499
|
-
const envPath =
|
|
500
|
-
if (
|
|
1435
|
+
const encPath = path4.join(dir, ".env.enc");
|
|
1436
|
+
const envPath = path4.join(dir, ".env");
|
|
1437
|
+
if (fs3.existsSync(encPath) && !passphrase) {
|
|
501
1438
|
console.log("");
|
|
502
1439
|
console.log(" Encrypted config found (.env.enc)");
|
|
503
1440
|
console.log("");
|
|
@@ -505,7 +1442,7 @@ program.command("export-key").description("Display your trading wallet private k
|
|
|
505
1442
|
console.log("");
|
|
506
1443
|
}
|
|
507
1444
|
const usedEncrypted = loadSecureEnv(dir, passphrase);
|
|
508
|
-
if (!usedEncrypted &&
|
|
1445
|
+
if (!usedEncrypted && fs3.existsSync(envPath)) {
|
|
509
1446
|
const { config: loadDotenv } = await import("dotenv");
|
|
510
1447
|
loadDotenv({ path: envPath, override: true });
|
|
511
1448
|
}
|
|
@@ -543,4 +1480,132 @@ program.command("export-key").description("Display your trading wallet private k
|
|
|
543
1480
|
process.exit(1);
|
|
544
1481
|
}
|
|
545
1482
|
});
|
|
1483
|
+
program.command("backtest").description("Run a historical backtest of your strategy against past market data").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").option("--start <date>", "Start date (YYYY-MM-DD), default: 6 months ago").option("--end <date>", "End date (YYYY-MM-DD), default: today").option("--interval <interval>", "Candle interval: daily or hourly", "daily").option("--initial-eth <amount>", "Initial ETH balance", "0.5").option("--initial-usdc <amount>", "Initial USDC balance", "0").option("--tokens <addresses>", "Comma-separated token addresses to track").option("--fresh", "Clear price + LLM caches before starting").option("--no-cache", "Skip LLM caching (always make real calls)").action(async (options) => {
|
|
1484
|
+
try {
|
|
1485
|
+
const configDir = path4.dirname(
|
|
1486
|
+
options.config.startsWith("/") ? options.config : path4.join(process.cwd(), options.config)
|
|
1487
|
+
);
|
|
1488
|
+
let passphrase = options.passphrase || process.env.EXAGENT_PASSPHRASE;
|
|
1489
|
+
const encPath = path4.join(configDir, ".env.enc");
|
|
1490
|
+
if (fs3.existsSync(encPath) && !passphrase) {
|
|
1491
|
+
console.log("");
|
|
1492
|
+
console.log(" Encrypted config found (.env.enc)");
|
|
1493
|
+
console.log("");
|
|
1494
|
+
passphrase = await prompt(" Enter passphrase: ", true);
|
|
1495
|
+
console.log("");
|
|
1496
|
+
}
|
|
1497
|
+
const usedEncrypted = loadSecureEnv(configDir, passphrase);
|
|
1498
|
+
if (usedEncrypted) {
|
|
1499
|
+
console.log("Loaded encrypted environment (.env.enc)");
|
|
1500
|
+
}
|
|
1501
|
+
const config = loadConfig(options.config);
|
|
1502
|
+
const now = Date.now();
|
|
1503
|
+
const sixMonthsAgo = now - 180 * 24 * 60 * 60 * 1e3;
|
|
1504
|
+
const startTime = options.start ? (/* @__PURE__ */ new Date(options.start + "T00:00:00Z")).getTime() : sixMonthsAgo;
|
|
1505
|
+
const endTime = options.end ? (/* @__PURE__ */ new Date(options.end + "T23:59:59Z")).getTime() : now;
|
|
1506
|
+
if (isNaN(startTime) || isNaN(endTime)) {
|
|
1507
|
+
console.error(" ERROR: Invalid date format. Use YYYY-MM-DD.");
|
|
1508
|
+
process.exit(1);
|
|
1509
|
+
}
|
|
1510
|
+
if (startTime >= endTime) {
|
|
1511
|
+
console.error(" ERROR: Start date must be before end date.");
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
}
|
|
1514
|
+
const interval = options.interval === "hourly" ? "hourly" : "daily";
|
|
1515
|
+
const NATIVE_ETH2 = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1516
|
+
const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
1517
|
+
const WETH_BASE2 = "0x4200000000000000000000000000000000000006";
|
|
1518
|
+
const initialBalances = {};
|
|
1519
|
+
const ethAmount = parseFloat(options.initialEth || "0.5");
|
|
1520
|
+
if (ethAmount > 0) {
|
|
1521
|
+
initialBalances[NATIVE_ETH2] = BigInt(Math.floor(ethAmount * 1e18));
|
|
1522
|
+
}
|
|
1523
|
+
const usdcAmount = parseFloat(options.initialUsdc || "0");
|
|
1524
|
+
if (usdcAmount > 0) {
|
|
1525
|
+
initialBalances[USDC_BASE] = BigInt(Math.floor(usdcAmount * 1e6));
|
|
1526
|
+
}
|
|
1527
|
+
let tokenAddresses = [];
|
|
1528
|
+
if (options.tokens) {
|
|
1529
|
+
tokenAddresses = options.tokens.split(",").map((t) => t.trim().toLowerCase());
|
|
1530
|
+
} else if (config.allowedTokens && config.allowedTokens.length > 0) {
|
|
1531
|
+
tokenAddresses = config.allowedTokens.map((t) => t.toLowerCase());
|
|
1532
|
+
}
|
|
1533
|
+
const baseTokens = [NATIVE_ETH2, USDC_BASE, WETH_BASE2];
|
|
1534
|
+
for (const t of baseTokens) {
|
|
1535
|
+
if (!tokenAddresses.includes(t.toLowerCase())) {
|
|
1536
|
+
tokenAddresses.push(t.toLowerCase());
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const durationDays = Math.ceil((endTime - startTime) / (24 * 60 * 60 * 1e3));
|
|
1540
|
+
const estimatedCycles = interval === "daily" ? durationDays : durationDays * 24;
|
|
1541
|
+
const estimatedCostLow = estimatedCycles * 0.01;
|
|
1542
|
+
const estimatedCostHigh = estimatedCycles * 0.1;
|
|
1543
|
+
console.log("");
|
|
1544
|
+
console.log("=".repeat(60));
|
|
1545
|
+
console.log(" EXAGENT HISTORICAL BACKTEST");
|
|
1546
|
+
console.log(" Strategy refinement tool");
|
|
1547
|
+
console.log("=".repeat(60));
|
|
1548
|
+
console.log("");
|
|
1549
|
+
console.log(` Agent: ${config.name} (ID: ${config.agentId})`);
|
|
1550
|
+
console.log(` LLM: ${config.llm.provider} (${config.llm.model || "default"})`);
|
|
1551
|
+
console.log(` Universe: ${config.riskUniverse}`);
|
|
1552
|
+
console.log(` Period: ${new Date(startTime).toISOString().split("T")[0]} \u2192 ${new Date(endTime).toISOString().split("T")[0]} (${durationDays} days)`);
|
|
1553
|
+
console.log(` Interval: ${interval} (~${estimatedCycles} cycles)`);
|
|
1554
|
+
console.log(` Tokens: ${tokenAddresses.length}`);
|
|
1555
|
+
if (ethAmount > 0) console.log(` Initial ETH: ${ethAmount}`);
|
|
1556
|
+
if (usdcAmount > 0) console.log(` Initial USDC: ${usdcAmount}`);
|
|
1557
|
+
console.log("");
|
|
1558
|
+
console.log("\u2500".repeat(60));
|
|
1559
|
+
console.log(" BACKTESTING NOTICE");
|
|
1560
|
+
console.log("\u2500".repeat(60));
|
|
1561
|
+
console.log("");
|
|
1562
|
+
console.log(" - This backtest will make real LLM API calls using your");
|
|
1563
|
+
console.log(" configured provider (results cached for re-runs).");
|
|
1564
|
+
console.log(` - Estimated cost: ~$${estimatedCostLow.toFixed(2)}-$${estimatedCostHigh.toFixed(2)} (${estimatedCycles} cycles)`);
|
|
1565
|
+
console.log(" - Historical results do not guarantee future performance.");
|
|
1566
|
+
console.log(" - LLM strategies are non-deterministic \u2014 running the same");
|
|
1567
|
+
console.log(" backtest twice may produce different results.");
|
|
1568
|
+
console.log(" - The LLM may have knowledge of historical market events");
|
|
1569
|
+
console.log(" in its training data. This can bias results optimistically.");
|
|
1570
|
+
console.log("");
|
|
1571
|
+
console.log(" Paper trading (forward test) provides the most honest");
|
|
1572
|
+
console.log(" evaluation of an LLM strategy.");
|
|
1573
|
+
if (!options.cache) {
|
|
1574
|
+
console.log("");
|
|
1575
|
+
console.log(" --no-cache is ON: All LLM calls will be real (no caching).");
|
|
1576
|
+
}
|
|
1577
|
+
console.log("");
|
|
1578
|
+
console.log("=".repeat(60));
|
|
1579
|
+
console.log("");
|
|
1580
|
+
await prompt(" Press Enter to start backtest...");
|
|
1581
|
+
console.log("");
|
|
1582
|
+
const { BacktestRunner: BacktestRunner2 } = (init_runner(), __toCommonJS(runner_exports));
|
|
1583
|
+
const runner = new BacktestRunner2({
|
|
1584
|
+
agentConfig: config,
|
|
1585
|
+
startTime,
|
|
1586
|
+
endTime,
|
|
1587
|
+
interval,
|
|
1588
|
+
initialBalances,
|
|
1589
|
+
tokenAddresses,
|
|
1590
|
+
noLLMCache: !options.cache,
|
|
1591
|
+
freshStart: options.fresh
|
|
1592
|
+
});
|
|
1593
|
+
await runner.run((progress) => {
|
|
1594
|
+
const pct = ((progress.step + 1) / progress.total * 100).toFixed(0);
|
|
1595
|
+
const barLen = 20;
|
|
1596
|
+
const filled = Math.round((progress.step + 1) / progress.total * barLen);
|
|
1597
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
1598
|
+
const tradeStr = progress.tradesThisStep > 0 ? ` ${progress.tradesThisStep} trade${progress.tradesThisStep > 1 ? "s" : ""}` : "";
|
|
1599
|
+
process.stdout.write(
|
|
1600
|
+
`\r [${progress.step + 1}/${progress.total}] ${progress.date} $${progress.portfolioValue.toFixed(0)} ${bar} ${pct}%${tradeStr} `
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
1604
|
+
process.exit(0);
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
console.error("");
|
|
1607
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1608
|
+
process.exit(1);
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
546
1611
|
program.parse();
|