@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/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-27O4UUAA.mjs";
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&currency=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 fs from "fs";
16
- import * as path from "path";
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 = path.join(path.dirname(configPath), ".env");
866
+ const envPath = path4.join(path4.dirname(configPath), ".env");
63
867
  const encPath = envPath + ".enc";
64
- if (fs.existsSync(encPath)) {
868
+ if (fs3.existsSync(encPath)) {
65
869
  return;
66
870
  }
67
- if (fs.existsSync(envPath)) {
68
- const envContent2 = fs.readFileSync(envPath, "utf-8");
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
- fs.writeFileSync(envPath, envContent, { mode: 384 });
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 = path.dirname(
310
- options.config.startsWith("/") ? options.config : path.join(process.cwd(), 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 = path.join(configDir, ".env.enc");
314
- if (fs.existsSync(encPath) && !passphrase) {
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 : path.join(process.cwd(), options.dir);
441
- const envPath = path.join(dir, ".env");
442
- if (!fs.existsSync(envPath)) {
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 (fs.existsSync(encPath)) {
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 : path.join(process.cwd(), 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 = path.join(dir, ".env.enc");
499
- const envPath = path.join(dir, ".env");
500
- if (fs.existsSync(encPath) && !passphrase) {
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 && fs.existsSync(envPath)) {
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();