@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/index.js CHANGED
@@ -1635,7 +1635,7 @@ async function loadStrategy(strategyPath) {
1635
1635
  console.log("No custom strategy found, using default (hold) strategy");
1636
1636
  return defaultStrategy;
1637
1637
  }
1638
- async function loadTypeScriptModule(path2) {
1638
+ async function loadTypeScriptModule(path4) {
1639
1639
  try {
1640
1640
  const tsxPath = require.resolve("tsx");
1641
1641
  const { pathToFileURL } = await import("url");
@@ -1646,7 +1646,7 @@ async function loadTypeScriptModule(path2) {
1646
1646
  "--import",
1647
1647
  "tsx/esm",
1648
1648
  "-e",
1649
- `import('${pathToFileURL(path2).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
1649
+ `import('${pathToFileURL(path4).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
1650
1650
  ],
1651
1651
  {
1652
1652
  cwd: process.cwd(),
@@ -1669,7 +1669,7 @@ async function loadTypeScriptModule(path2) {
1669
1669
  const tsx = await import("tsx/esm/api");
1670
1670
  const unregister = tsx.register();
1671
1671
  try {
1672
- const module2 = await import(path2);
1672
+ const module2 = await import(path4);
1673
1673
  return module2;
1674
1674
  } finally {
1675
1675
  unregister();
@@ -3144,6 +3144,609 @@ function openBrowser(url) {
3144
3144
  }
3145
3145
  }
3146
3146
 
3147
+ // src/paper/executor.ts
3148
+ var PaperExecutor = class {
3149
+ config;
3150
+ allowedTokens;
3151
+ prices;
3152
+ slippageBps;
3153
+ feeRateBps;
3154
+ estimatedGasUSD;
3155
+ /** Running totals for the paper session */
3156
+ totalFeesPaid = 0;
3157
+ totalGasPaid = 0;
3158
+ tradeCount = 0;
3159
+ constructor(config, options) {
3160
+ this.config = config;
3161
+ this.slippageBps = options?.slippageBps ?? 50;
3162
+ this.feeRateBps = options?.feeRateBps ?? 20;
3163
+ this.estimatedGasUSD = options?.estimatedGasUSD ?? 0.01;
3164
+ this.prices = {};
3165
+ this.allowedTokens = new Set(
3166
+ (config.allowedTokens || []).map((t) => t.toLowerCase())
3167
+ );
3168
+ }
3169
+ /**
3170
+ * Update live prices before executing trades.
3171
+ * Called by the runtime before each cycle with current market data.
3172
+ */
3173
+ updatePrices(prices) {
3174
+ this.prices = prices;
3175
+ }
3176
+ /**
3177
+ * Simulate a single trade.
3178
+ * Returns the same shape as TradeExecutor.execute() for drop-in compatibility.
3179
+ */
3180
+ async execute(signal) {
3181
+ if (signal.action === "hold") {
3182
+ return { success: true };
3183
+ }
3184
+ if (!this.validateSignal(signal)) {
3185
+ return { success: false, error: "Signal failed validation" };
3186
+ }
3187
+ const tokenInPrice = this.prices[signal.tokenIn.toLowerCase()];
3188
+ const tokenOutPrice = this.prices[signal.tokenOut.toLowerCase()];
3189
+ if (!tokenInPrice || !tokenOutPrice) {
3190
+ return {
3191
+ success: false,
3192
+ error: `Missing price data: ${!tokenInPrice ? signal.tokenIn : signal.tokenOut}`
3193
+ };
3194
+ }
3195
+ const fill = this.simulateFill(signal, tokenInPrice, tokenOutPrice);
3196
+ this.tradeCount++;
3197
+ this.totalFeesPaid += fill.feeUSD;
3198
+ this.totalGasPaid += this.estimatedGasUSD;
3199
+ console.log(
3200
+ ` [PAPER] ${signal.action.toUpperCase()}: ${signal.tokenIn.slice(0, 10)}... \u2192 ${signal.tokenOut.slice(0, 10)}...`
3201
+ );
3202
+ console.log(
3203
+ ` [PAPER] Fill: $${fill.valueInUSD.toFixed(2)} \u2192 $${fill.valueOutUSD.toFixed(2)} (slippage: ${fill.appliedSlippageBps}bps, fee: $${fill.feeUSD.toFixed(4)})`
3204
+ );
3205
+ return {
3206
+ success: true,
3207
+ txHash: `paper-${Date.now()}-${this.tradeCount}`,
3208
+ paperFill: fill
3209
+ };
3210
+ }
3211
+ /**
3212
+ * Simulate multiple trades (same interface as TradeExecutor.executeAll).
3213
+ */
3214
+ async executeAll(signals) {
3215
+ const results = [];
3216
+ for (const signal of signals) {
3217
+ const result = await this.execute(signal);
3218
+ results.push({ signal, ...result });
3219
+ }
3220
+ return results;
3221
+ }
3222
+ /**
3223
+ * Get session totals for reporting.
3224
+ */
3225
+ getSessionStats() {
3226
+ return {
3227
+ tradeCount: this.tradeCount,
3228
+ totalFeesPaid: this.totalFeesPaid,
3229
+ totalGasPaid: this.totalGasPaid,
3230
+ slippageBps: this.slippageBps,
3231
+ feeRateBps: this.feeRateBps
3232
+ };
3233
+ }
3234
+ /**
3235
+ * Reset session stats (e.g., when starting a new paper session).
3236
+ */
3237
+ resetStats() {
3238
+ this.tradeCount = 0;
3239
+ this.totalFeesPaid = 0;
3240
+ this.totalGasPaid = 0;
3241
+ }
3242
+ // --- Private methods ---
3243
+ simulateFill(signal, tokenInPrice, tokenOutPrice) {
3244
+ const decimalsIn = getTokenDecimals(signal.tokenIn);
3245
+ const valueInUSD = Number(signal.amountIn) / Math.pow(10, decimalsIn) * tokenInPrice;
3246
+ const afterFeeUSD = valueInUSD * (1 - this.feeRateBps / 1e4);
3247
+ const feeUSD = valueInUSD - afterFeeUSD;
3248
+ const afterSlippageUSD = afterFeeUSD * (1 - this.slippageBps / 1e4);
3249
+ const decimalsOut = getTokenDecimals(signal.tokenOut);
3250
+ const amountOut = BigInt(Math.floor(afterSlippageUSD / tokenOutPrice * Math.pow(10, decimalsOut)));
3251
+ return {
3252
+ signal,
3253
+ timestamp: Date.now(),
3254
+ valueInUSD,
3255
+ valueOutUSD: afterSlippageUSD,
3256
+ feeUSD,
3257
+ gasUSD: this.estimatedGasUSD,
3258
+ amountOut,
3259
+ fillPriceUSD: tokenOutPrice * (1 + this.slippageBps / 1e4),
3260
+ // worse price due to slippage
3261
+ appliedSlippageBps: this.slippageBps
3262
+ };
3263
+ }
3264
+ validateSignal(signal) {
3265
+ if (signal.confidence < 0.5) {
3266
+ console.warn(` [PAPER] Signal confidence ${signal.confidence} below threshold (0.5)`);
3267
+ return false;
3268
+ }
3269
+ if (this.allowedTokens.size > 0) {
3270
+ const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
3271
+ const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
3272
+ if (!tokenInAllowed || !tokenOutAllowed) {
3273
+ console.warn(` [PAPER] Token not in allowed list \u2014 skipping`);
3274
+ return false;
3275
+ }
3276
+ }
3277
+ return true;
3278
+ }
3279
+ };
3280
+
3281
+ // src/paper/portfolio.ts
3282
+ var fs = __toESM(require("fs"));
3283
+ var path = __toESM(require("path"));
3284
+ var SimulatedPortfolio = class _SimulatedPortfolio {
3285
+ balances;
3286
+ initialBalances;
3287
+ equityCurve;
3288
+ trades;
3289
+ startedAt;
3290
+ dataDir;
3291
+ constructor(initialBalances, dataDir, options) {
3292
+ this.balances = {};
3293
+ this.initialBalances = {};
3294
+ for (const [addr, amount] of Object.entries(initialBalances)) {
3295
+ const key = addr.toLowerCase();
3296
+ this.balances[key] = amount;
3297
+ this.initialBalances[key] = amount;
3298
+ }
3299
+ this.equityCurve = [];
3300
+ this.trades = [];
3301
+ this.startedAt = options?.startedAt ?? Date.now();
3302
+ this.dataDir = dataDir;
3303
+ if (!fs.existsSync(dataDir)) {
3304
+ fs.mkdirSync(dataDir, { recursive: true });
3305
+ }
3306
+ }
3307
+ /**
3308
+ * Get the session start timestamp.
3309
+ */
3310
+ getStartedAt() {
3311
+ return this.startedAt;
3312
+ }
3313
+ /**
3314
+ * Get current balances in the same format as MarketData.balances.
3315
+ * Used by the runtime to build MarketData for strategy calls.
3316
+ */
3317
+ getBalances() {
3318
+ return { ...this.balances };
3319
+ }
3320
+ /**
3321
+ * Apply a paper trade fill to the portfolio.
3322
+ * Deducts tokenIn, adds tokenOut.
3323
+ */
3324
+ applyFill(fill) {
3325
+ const tokenIn = fill.signal.tokenIn.toLowerCase();
3326
+ const tokenOut = fill.signal.tokenOut.toLowerCase();
3327
+ const currentIn = this.balances[tokenIn] || BigInt(0);
3328
+ const newIn = currentIn - fill.signal.amountIn;
3329
+ if (newIn < BigInt(0)) {
3330
+ console.warn(` [PAPER] Warning: balance went negative for ${tokenIn} \u2014 clamping to 0`);
3331
+ this.balances[tokenIn] = BigInt(0);
3332
+ } else {
3333
+ this.balances[tokenIn] = newIn;
3334
+ }
3335
+ const currentOut = this.balances[tokenOut] || BigInt(0);
3336
+ this.balances[tokenOut] = currentOut + fill.amountOut;
3337
+ this.trades.push({
3338
+ timestamp: fill.timestamp,
3339
+ action: fill.signal.action,
3340
+ tokenIn,
3341
+ tokenOut,
3342
+ amountIn: fill.signal.amountIn.toString(),
3343
+ amountOut: fill.amountOut.toString(),
3344
+ valueInUSD: fill.valueInUSD,
3345
+ valueOutUSD: fill.valueOutUSD,
3346
+ feeUSD: fill.feeUSD,
3347
+ gasUSD: fill.gasUSD,
3348
+ reasoning: fill.signal.reasoning
3349
+ });
3350
+ }
3351
+ /**
3352
+ * Record a portfolio value snapshot (called after each cycle).
3353
+ * @param prices - Current token prices
3354
+ * @param timestamp - Optional timestamp override (for backtesting with historical timestamps)
3355
+ */
3356
+ recordEquityPoint(prices, timestamp) {
3357
+ const value = this.calculateValue(prices);
3358
+ this.equityCurve.push({
3359
+ timestamp: timestamp ?? Date.now(),
3360
+ value
3361
+ });
3362
+ }
3363
+ /**
3364
+ * Calculate current portfolio value in USD.
3365
+ */
3366
+ calculateValue(prices) {
3367
+ let total = 0;
3368
+ for (const [token, balance] of Object.entries(this.balances)) {
3369
+ if (balance <= BigInt(0)) continue;
3370
+ const price = prices[token.toLowerCase()] || 0;
3371
+ const decimals = getTokenDecimals(token);
3372
+ total += Number(balance) / Math.pow(10, decimals) * price;
3373
+ }
3374
+ return total;
3375
+ }
3376
+ /**
3377
+ * Calculate initial portfolio value for return comparison.
3378
+ */
3379
+ calculateInitialValue(prices) {
3380
+ let total = 0;
3381
+ for (const [token, balance] of Object.entries(this.initialBalances)) {
3382
+ if (balance <= BigInt(0)) continue;
3383
+ const price = prices[token.toLowerCase()] || 0;
3384
+ const decimals = getTokenDecimals(token);
3385
+ total += Number(balance) / Math.pow(10, decimals) * price;
3386
+ }
3387
+ return total;
3388
+ }
3389
+ /**
3390
+ * Get equity curve data.
3391
+ */
3392
+ getEquityCurve() {
3393
+ return [...this.equityCurve];
3394
+ }
3395
+ /**
3396
+ * Get all paper trades.
3397
+ */
3398
+ getTrades() {
3399
+ return [...this.trades];
3400
+ }
3401
+ /**
3402
+ * Get summary for display.
3403
+ */
3404
+ getSummary(prices) {
3405
+ const currentValue = this.calculateValue(prices);
3406
+ const initialValue = this.calculateInitialValue(prices);
3407
+ const totalReturn = initialValue > 0 ? (currentValue - initialValue) / initialValue * 100 : 0;
3408
+ const totalFees = this.trades.reduce((sum, t) => sum + t.feeUSD, 0);
3409
+ const totalGas = this.trades.reduce((sum, t) => sum + t.gasUSD, 0);
3410
+ return {
3411
+ startedAt: this.startedAt,
3412
+ currentValue,
3413
+ initialValue,
3414
+ totalReturnPct: totalReturn,
3415
+ totalReturnUSD: currentValue - initialValue,
3416
+ tradeCount: this.trades.length,
3417
+ totalFees,
3418
+ totalGas,
3419
+ positions: this.getPositionBreakdown(prices)
3420
+ };
3421
+ }
3422
+ /**
3423
+ * Persist portfolio state to disk.
3424
+ */
3425
+ save() {
3426
+ const filePath = path.join(this.dataDir, "paper-portfolio.json");
3427
+ const data = {
3428
+ startedAt: this.startedAt,
3429
+ balances: Object.fromEntries(
3430
+ Object.entries(this.balances).map(([k, v]) => [k, v.toString()])
3431
+ ),
3432
+ initialBalances: Object.fromEntries(
3433
+ Object.entries(this.initialBalances).map(([k, v]) => [k, v.toString()])
3434
+ ),
3435
+ equityCurve: this.equityCurve,
3436
+ trades: this.trades
3437
+ };
3438
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
3439
+ }
3440
+ /**
3441
+ * Load portfolio state from disk (for resuming a paper session).
3442
+ * Returns null if no saved state exists.
3443
+ */
3444
+ static load(dataDir) {
3445
+ const filePath = path.join(dataDir, "paper-portfolio.json");
3446
+ if (!fs.existsSync(filePath)) return null;
3447
+ try {
3448
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
3449
+ const initialBalances = {};
3450
+ for (const [k, v] of Object.entries(raw.initialBalances)) {
3451
+ initialBalances[k] = BigInt(v);
3452
+ }
3453
+ const portfolio = new _SimulatedPortfolio(initialBalances, dataDir);
3454
+ portfolio.startedAt = raw.startedAt;
3455
+ portfolio.equityCurve = raw.equityCurve || [];
3456
+ portfolio.trades = raw.trades || [];
3457
+ portfolio.balances = {};
3458
+ for (const [k, v] of Object.entries(raw.balances)) {
3459
+ portfolio.balances[k] = BigInt(v);
3460
+ }
3461
+ return portfolio;
3462
+ } catch {
3463
+ console.warn(" [PAPER] Failed to load saved portfolio state \u2014 starting fresh");
3464
+ return null;
3465
+ }
3466
+ }
3467
+ /**
3468
+ * Delete saved state (for starting a new paper session).
3469
+ */
3470
+ static clear(dataDir) {
3471
+ const filePath = path.join(dataDir, "paper-portfolio.json");
3472
+ if (fs.existsSync(filePath)) {
3473
+ fs.unlinkSync(filePath);
3474
+ }
3475
+ }
3476
+ // --- Private ---
3477
+ getPositionBreakdown(prices) {
3478
+ const positions = [];
3479
+ for (const [token, balance] of Object.entries(this.balances)) {
3480
+ if (balance <= BigInt(0)) continue;
3481
+ const price = prices[token.toLowerCase()] || 0;
3482
+ const decimals = getTokenDecimals(token);
3483
+ const valueUSD = Number(balance) / Math.pow(10, decimals) * price;
3484
+ if (valueUSD >= 0.01) {
3485
+ positions.push({
3486
+ token,
3487
+ balance: (Number(balance) / Math.pow(10, decimals)).toFixed(6),
3488
+ valueUSD
3489
+ });
3490
+ }
3491
+ }
3492
+ return positions.sort((a, b) => b.valueUSD - a.valueUSD);
3493
+ }
3494
+ };
3495
+
3496
+ // src/paper/results.ts
3497
+ var fs2 = __toESM(require("fs"));
3498
+ var path2 = __toESM(require("path"));
3499
+
3500
+ // src/paper/metrics.ts
3501
+ function calculateMetrics(trades, equityCurve, initialValue, currentValue, startedAt, endedAt) {
3502
+ const durationMs = (endedAt ?? Date.now()) - startedAt;
3503
+ const durationDays = durationMs / (1e3 * 60 * 60 * 24);
3504
+ const totalReturnUSD = currentValue - initialValue;
3505
+ const totalReturnPct = initialValue > 0 ? totalReturnUSD / initialValue * 100 : 0;
3506
+ const actionTrades = trades.filter((t) => t.action !== "hold");
3507
+ let wins = 0;
3508
+ let losses = 0;
3509
+ let grossProfit = 0;
3510
+ let grossLoss = 0;
3511
+ for (const trade of actionTrades) {
3512
+ const pnl = trade.valueOutUSD - trade.valueInUSD;
3513
+ if (pnl >= 0) {
3514
+ wins++;
3515
+ grossProfit += pnl;
3516
+ } else {
3517
+ losses++;
3518
+ grossLoss += Math.abs(pnl);
3519
+ }
3520
+ }
3521
+ const winRate = actionTrades.length > 0 ? wins / actionTrades.length : 0;
3522
+ const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : null;
3523
+ const { maxDrawdownPct, maxDrawdownUSD } = calculateMaxDrawdown(equityCurve);
3524
+ const { sharpe, sortino } = calculateRiskMetrics(equityCurve);
3525
+ const totalFees = trades.reduce((s, t) => s + t.feeUSD, 0);
3526
+ const totalGas = trades.reduce((s, t) => s + t.gasUSD, 0);
3527
+ const avgTradeValueUSD = actionTrades.length > 0 ? actionTrades.reduce((s, t) => s + t.valueInUSD, 0) / actionTrades.length : 0;
3528
+ const tradesPerDay = durationDays > 0 ? actionTrades.length / durationDays : 0;
3529
+ return {
3530
+ totalReturnPct,
3531
+ totalReturnUSD,
3532
+ winRate,
3533
+ wins,
3534
+ losses,
3535
+ maxDrawdownPct,
3536
+ maxDrawdownUSD,
3537
+ sharpeRatio: sharpe,
3538
+ sortinoRatio: sortino,
3539
+ profitFactor,
3540
+ avgTradeValueUSD,
3541
+ totalFees,
3542
+ totalGas,
3543
+ durationMs,
3544
+ tradesPerDay
3545
+ };
3546
+ }
3547
+ function calculateMaxDrawdown(curve) {
3548
+ if (curve.length < 2) return { maxDrawdownPct: 0, maxDrawdownUSD: 0 };
3549
+ let peak = curve[0].value;
3550
+ let maxDrawdownPct = 0;
3551
+ let maxDrawdownUSD = 0;
3552
+ for (const point of curve) {
3553
+ if (point.value > peak) {
3554
+ peak = point.value;
3555
+ }
3556
+ if (peak > 0) {
3557
+ const drawdownPct = (point.value - peak) / peak * 100;
3558
+ const drawdownUSD = point.value - peak;
3559
+ if (drawdownPct < maxDrawdownPct) {
3560
+ maxDrawdownPct = drawdownPct;
3561
+ maxDrawdownUSD = drawdownUSD;
3562
+ }
3563
+ }
3564
+ }
3565
+ return { maxDrawdownPct, maxDrawdownUSD };
3566
+ }
3567
+ function calculateRiskMetrics(curve) {
3568
+ if (curve.length < 3) return { sharpe: null, sortino: null };
3569
+ const returns = [];
3570
+ for (let i = 1; i < curve.length; i++) {
3571
+ if (curve[i - 1].value > 0) {
3572
+ returns.push((curve[i].value - curve[i - 1].value) / curve[i - 1].value);
3573
+ }
3574
+ }
3575
+ if (returns.length < 2) return { sharpe: null, sortino: null };
3576
+ const meanReturn = returns.reduce((s, r) => s + r, 0) / returns.length;
3577
+ const variance = returns.reduce((s, r) => s + (r - meanReturn) ** 2, 0) / (returns.length - 1);
3578
+ const stdDev = Math.sqrt(variance);
3579
+ const downsideVariance = returns.reduce((s, r) => {
3580
+ const downside = Math.min(0, r);
3581
+ return s + downside ** 2;
3582
+ }, 0) / (returns.length - 1);
3583
+ const downsideDev = Math.sqrt(downsideVariance);
3584
+ const totalTimeMs = curve[curve.length - 1].timestamp - curve[0].timestamp;
3585
+ const avgPeriodMs = totalTimeMs / (curve.length - 1);
3586
+ const periodsPerYear = avgPeriodMs > 0 ? 365 * 24 * 60 * 60 * 1e3 / avgPeriodMs : 365;
3587
+ const sharpe = stdDev > 0 ? meanReturn / stdDev * Math.sqrt(periodsPerYear) : null;
3588
+ const sortino = downsideDev > 0 ? meanReturn / downsideDev * Math.sqrt(periodsPerYear) : null;
3589
+ return { sharpe, sortino };
3590
+ }
3591
+
3592
+ // src/paper/results.ts
3593
+ function saveSessionResult(dataDir, agentName, llm, portfolio, equityCurve, trades, startedAt, options) {
3594
+ const endedAt = options?.endedAt ?? Date.now();
3595
+ const durationMs = endedAt - startedAt;
3596
+ const prefix = options?.idPrefix ?? "paper";
3597
+ const id = `${prefix}-${startedAt}`;
3598
+ const metrics = calculateMetrics(
3599
+ trades,
3600
+ equityCurve,
3601
+ portfolio.initialValue,
3602
+ portfolio.currentValue,
3603
+ startedAt,
3604
+ endedAt
3605
+ );
3606
+ const result = {
3607
+ id,
3608
+ agentName,
3609
+ startedAt,
3610
+ endedAt,
3611
+ durationMs,
3612
+ llm,
3613
+ initialValue: portfolio.initialValue,
3614
+ finalValue: portfolio.currentValue,
3615
+ metrics,
3616
+ portfolio,
3617
+ equityCurve,
3618
+ trades
3619
+ };
3620
+ const sessionsDir = path2.join(dataDir, "sessions");
3621
+ if (!fs2.existsSync(sessionsDir)) {
3622
+ fs2.mkdirSync(sessionsDir, { recursive: true });
3623
+ }
3624
+ const filePath = path2.join(sessionsDir, `${id}.json`);
3625
+ fs2.writeFileSync(filePath, JSON.stringify(result, null, 2));
3626
+ console.log(` [PAPER] Session saved: ${filePath}`);
3627
+ return result;
3628
+ }
3629
+ function formatSessionReport(result) {
3630
+ const m = result.metrics;
3631
+ const lines = [];
3632
+ const divider = "\u2550".repeat(56);
3633
+ const thinDivider = "\u2500".repeat(56);
3634
+ lines.push("");
3635
+ lines.push(` ${divider}`);
3636
+ lines.push(` PAPER TRADING RESULTS \u2014 ${result.agentName}`);
3637
+ lines.push(` ${divider}`);
3638
+ lines.push("");
3639
+ lines.push(` Session: ${result.id}`);
3640
+ lines.push(` Started: ${new Date(result.startedAt).toLocaleString()}`);
3641
+ lines.push(` Ended: ${new Date(result.endedAt).toLocaleString()}`);
3642
+ lines.push(` Duration: ${formatDuration(result.durationMs)}`);
3643
+ lines.push(` LLM: ${result.llm.provider}/${result.llm.model}`);
3644
+ lines.push("");
3645
+ lines.push(` ${thinDivider}`);
3646
+ lines.push(" PERFORMANCE");
3647
+ lines.push(` ${thinDivider}`);
3648
+ lines.push("");
3649
+ const returnSign = m.totalReturnPct >= 0 ? "+" : "";
3650
+ lines.push(` Total Return: ${returnSign}${m.totalReturnPct.toFixed(2)}% ($${m.totalReturnUSD.toFixed(2)})`);
3651
+ lines.push(` Initial Value: $${result.initialValue.toFixed(2)}`);
3652
+ lines.push(` Final Value: $${result.finalValue.toFixed(2)}`);
3653
+ lines.push("");
3654
+ lines.push(` Max Drawdown: ${m.maxDrawdownPct.toFixed(2)}% ($${m.maxDrawdownUSD.toFixed(2)})`);
3655
+ lines.push(` Sharpe Ratio: ${m.sharpeRatio !== null ? m.sharpeRatio.toFixed(2) : "N/A"}`);
3656
+ lines.push(` Sortino Ratio: ${m.sortinoRatio !== null ? m.sortinoRatio.toFixed(2) : "N/A"}`);
3657
+ lines.push("");
3658
+ lines.push(` ${thinDivider}`);
3659
+ lines.push(" TRADES");
3660
+ lines.push(` ${thinDivider}`);
3661
+ lines.push("");
3662
+ lines.push(` Total Trades: ${result.trades.length}`);
3663
+ lines.push(` Win Rate: ${(m.winRate * 100).toFixed(1)}% (${m.wins}W / ${m.losses}L)`);
3664
+ lines.push(` Profit Factor: ${m.profitFactor !== null ? m.profitFactor.toFixed(2) : "N/A"}`);
3665
+ lines.push(` Avg Trade Size: $${m.avgTradeValueUSD.toFixed(2)}`);
3666
+ lines.push(` Trades/Day: ${m.tradesPerDay.toFixed(1)}`);
3667
+ lines.push("");
3668
+ lines.push(` ${thinDivider}`);
3669
+ lines.push(" COSTS (SIMULATED)");
3670
+ lines.push(` ${thinDivider}`);
3671
+ lines.push("");
3672
+ lines.push(` Total Fees: $${m.totalFees.toFixed(4)}`);
3673
+ lines.push(` Total Gas: $${m.totalGas.toFixed(4)}`);
3674
+ lines.push(` Total Costs: $${(m.totalFees + m.totalGas).toFixed(4)}`);
3675
+ lines.push("");
3676
+ if (result.portfolio.positions.length > 0) {
3677
+ lines.push(` ${thinDivider}`);
3678
+ lines.push(" FINAL POSITIONS");
3679
+ lines.push(` ${thinDivider}`);
3680
+ lines.push("");
3681
+ for (const pos of result.portfolio.positions) {
3682
+ const tokenShort = pos.token.slice(0, 10) + "...";
3683
+ lines.push(` ${tokenShort} ${pos.balance} $${pos.valueUSD.toFixed(2)}`);
3684
+ }
3685
+ lines.push("");
3686
+ }
3687
+ if (result.equityCurve.length >= 3) {
3688
+ lines.push(` ${thinDivider}`);
3689
+ lines.push(" EQUITY CURVE");
3690
+ lines.push(` ${thinDivider}`);
3691
+ lines.push("");
3692
+ lines.push(renderAsciiChart(result.equityCurve));
3693
+ lines.push("");
3694
+ }
3695
+ lines.push(` ${divider}`);
3696
+ lines.push(" DISCLAIMER: Simulated results do not represent actual");
3697
+ lines.push(" trading. Past performance does not guarantee future");
3698
+ lines.push(" results. LLM strategies are non-deterministic. Real");
3699
+ lines.push(" trading may differ due to slippage, liquidity, MEV,");
3700
+ lines.push(" and execution timing.");
3701
+ lines.push(` ${divider}`);
3702
+ lines.push("");
3703
+ return lines.join("\n");
3704
+ }
3705
+ function formatDuration(ms) {
3706
+ const seconds = Math.floor(ms / 1e3);
3707
+ if (seconds < 60) return `${seconds}s`;
3708
+ const minutes = Math.floor(seconds / 60);
3709
+ if (minutes < 60) return `${minutes}m`;
3710
+ const hours = Math.floor(minutes / 60);
3711
+ const mins = minutes % 60;
3712
+ if (hours < 24) return `${hours}h ${mins}m`;
3713
+ const days = Math.floor(hours / 24);
3714
+ return `${days}d ${hours % 24}h`;
3715
+ }
3716
+ function renderAsciiChart(curve) {
3717
+ const width = 48;
3718
+ const height = 8;
3719
+ const step = Math.max(1, Math.floor(curve.length / width));
3720
+ const sampled = curve.filter((_, i) => i % step === 0).slice(0, width);
3721
+ const values = sampled.map((p) => p.value);
3722
+ const min = Math.min(...values);
3723
+ const max = Math.max(...values);
3724
+ const range = max - min || 1;
3725
+ const lines = [];
3726
+ for (let row = height - 1; row >= 0; row--) {
3727
+ const threshold = min + range * row / (height - 1);
3728
+ let line = " \u2502";
3729
+ for (const val of values) {
3730
+ const normalized = (val - min) / range * (height - 1);
3731
+ if (Math.round(normalized) === row) {
3732
+ line += "\u2588";
3733
+ } else if (Math.round(normalized) > row) {
3734
+ line += "\u2591";
3735
+ } else {
3736
+ line += " ";
3737
+ }
3738
+ }
3739
+ if (row === height - 1) {
3740
+ line += ` $${max.toFixed(0)}`;
3741
+ } else if (row === 0) {
3742
+ line += ` $${min.toFixed(0)}`;
3743
+ }
3744
+ lines.push(line);
3745
+ }
3746
+ lines.push(" \u2514" + "\u2500".repeat(values.length));
3747
+ return lines.join("\n");
3748
+ }
3749
+
3147
3750
  // src/perp/client.ts
3148
3751
  var HyperliquidClient = class {
3149
3752
  apiUrl;
@@ -4413,6 +5016,11 @@ var AgentRuntime = class {
4413
5016
  allowedTokens = /* @__PURE__ */ new Set();
4414
5017
  strategyContext;
4415
5018
  positionTracker;
5019
+ // Paper trading components (null when not in paper mode)
5020
+ paperExecutor = null;
5021
+ paperPortfolio = null;
5022
+ /** Whether agent was started directly in paper mode via CLI */
5023
+ startInPaperMode = false;
4416
5024
  // Perp trading components (null if perp not enabled)
4417
5025
  perpClient = null;
4418
5026
  perpSigner = null;
@@ -4435,9 +5043,13 @@ var AgentRuntime = class {
4435
5043
  cachedPerpMarginUsed = 0;
4436
5044
  cachedPerpLeverage = 0;
4437
5045
  cachedPerpOpenPositions = 0;
4438
- constructor(config) {
5046
+ constructor(config, options) {
4439
5047
  this.config = config;
5048
+ this.startInPaperMode = options?.paperMode ?? false;
5049
+ this._paperInitialBalances = options?.paperBalances;
4440
5050
  }
5051
+ /** Initial balances override for paper mode (from CLI --initial-eth/--initial-usdc) */
5052
+ _paperInitialBalances;
4441
5053
  /**
4442
5054
  * Initialize the agent runtime
4443
5055
  */
@@ -4890,16 +5502,20 @@ var AgentRuntime = class {
4890
5502
  } catch (error) {
4891
5503
  console.warn("Initial balance sync failed (non-fatal):", error instanceof Error ? error.message : error);
4892
5504
  }
5505
+ if (this.startInPaperMode) {
5506
+ await this.startPaperTrading(this._paperInitialBalances);
5507
+ }
4893
5508
  this.sendRelayStatus();
4894
5509
  this.relay.sendMessage(
4895
5510
  "system",
4896
5511
  "success",
4897
5512
  "Agent Connected",
4898
- `${this.config.name} is online and waiting for commands.`,
5513
+ `${this.config.name} is online${this.mode === "paper" ? " in PAPER TRADING mode" : " and waiting for commands"}.`,
4899
5514
  { wallet: this.client.address }
4900
5515
  );
4901
5516
  while (this.processAlive) {
4902
- if (this.mode === "trading" && this.isRunning) {
5517
+ const currentMode = this.mode;
5518
+ if ((currentMode === "trading" || currentMode === "paper") && this.isRunning) {
4903
5519
  try {
4904
5520
  await this.runCycle();
4905
5521
  } catch (error) {
@@ -4923,8 +5539,13 @@ var AgentRuntime = class {
4923
5539
  throw new Error("Agent is already running");
4924
5540
  }
4925
5541
  this.isRunning = true;
4926
- this.mode = "trading";
4927
- console.log("Starting trading loop...");
5542
+ if (this.startInPaperMode) {
5543
+ await this.startPaperTrading(this._paperInitialBalances);
5544
+ console.log("Starting paper trading loop...");
5545
+ } else {
5546
+ this.mode = "trading";
5547
+ console.log("Starting trading loop...");
5548
+ }
4928
5549
  console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
4929
5550
  while (this.isRunning) {
4930
5551
  try {
@@ -5171,6 +5792,41 @@ var AgentRuntime = class {
5171
5792
  }
5172
5793
  break;
5173
5794
  }
5795
+ case "start_paper_trading":
5796
+ if (this.mode === "paper") {
5797
+ this.relay?.sendCommandResult(cmd.id, true, "Already paper trading");
5798
+ return;
5799
+ }
5800
+ try {
5801
+ await this.startPaperTrading();
5802
+ this.relay?.sendCommandResult(cmd.id, true, "Paper trading started");
5803
+ this.relay?.sendMessage(
5804
+ "system",
5805
+ "success",
5806
+ "Paper Trading Started",
5807
+ "Agent is now paper trading with simulated execution. No on-chain transactions will be made. Results are estimates \u2014 real trading may differ due to slippage, liquidity, and execution timing."
5808
+ );
5809
+ } catch (error) {
5810
+ const msg = error instanceof Error ? error.message : String(error);
5811
+ this.relay?.sendCommandResult(cmd.id, false, `Failed to start paper trading: ${msg}`);
5812
+ }
5813
+ this.sendRelayStatus();
5814
+ break;
5815
+ case "stop_paper_trading":
5816
+ if (this.mode !== "paper") {
5817
+ this.relay?.sendCommandResult(cmd.id, true, "Not paper trading");
5818
+ return;
5819
+ }
5820
+ this.stopPaperTrading();
5821
+ this.relay?.sendCommandResult(cmd.id, true, "Paper trading stopped");
5822
+ this.relay?.sendMessage(
5823
+ "system",
5824
+ "info",
5825
+ "Paper Trading Stopped",
5826
+ `Paper session ended. ${this.paperPortfolio ? `Trades: ${this.paperPortfolio.getTrades().length}` : ""}`
5827
+ );
5828
+ this.sendRelayStatus();
5829
+ break;
5174
5830
  case "refresh_status":
5175
5831
  this.sendRelayStatus();
5176
5832
  this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
@@ -5197,6 +5853,63 @@ var AgentRuntime = class {
5197
5853
  this.relay?.sendCommandResult(cmd.id, false, message);
5198
5854
  }
5199
5855
  }
5856
+ // --- Paper Trading ---
5857
+ /**
5858
+ * Initialize and enter paper trading mode.
5859
+ * Snapshots current on-chain balances (or uses provided overrides) as starting state.
5860
+ */
5861
+ async startPaperTrading(initialBalances) {
5862
+ console.log("");
5863
+ console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
5864
+ console.log(" \u2502 PAPER TRADING MODE \u2502");
5865
+ console.log(" \u2502 Simulated execution \u2014 no on-chain trades \u2502");
5866
+ console.log(" \u2502 Results are estimates only. \u2502");
5867
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
5868
+ console.log("");
5869
+ const paperDataDir = require("path").join(process.cwd(), "data", "paper");
5870
+ if (initialBalances) {
5871
+ this.paperPortfolio = new SimulatedPortfolio(initialBalances, paperDataDir);
5872
+ console.log(" [PAPER] Using custom initial balances");
5873
+ } else {
5874
+ const existing = SimulatedPortfolio.load(paperDataDir);
5875
+ if (existing) {
5876
+ this.paperPortfolio = existing;
5877
+ console.log(" [PAPER] Resumed existing paper session");
5878
+ } else {
5879
+ const tokens = this.getTokensToTrack();
5880
+ const realData = await this.marketData.fetchMarketData(this.client.address, tokens);
5881
+ this.paperPortfolio = new SimulatedPortfolio(realData.balances, paperDataDir);
5882
+ console.log(` [PAPER] Snapshotted on-chain balances ($${realData.portfolioValue.toFixed(2)})`);
5883
+ }
5884
+ }
5885
+ this.paperExecutor = new PaperExecutor(this.config);
5886
+ this.mode = "paper";
5887
+ this.isRunning = true;
5888
+ }
5889
+ /**
5890
+ * Exit paper trading mode and return to idle.
5891
+ * Saves the paper portfolio state to disk.
5892
+ */
5893
+ stopPaperTrading() {
5894
+ if (this.paperPortfolio) {
5895
+ this.paperPortfolio.save();
5896
+ const summary = this.paperPortfolio.getSummary(this.lastPrices);
5897
+ const dataDir = require("path").join(process.cwd(), "data", "paper");
5898
+ const result = saveSessionResult(
5899
+ dataDir,
5900
+ this.config.name,
5901
+ { provider: this.config.llm.provider, model: this.config.llm.model || "default" },
5902
+ summary,
5903
+ this.paperPortfolio.getEquityCurve(),
5904
+ this.paperPortfolio.getTrades(),
5905
+ summary.startedAt
5906
+ );
5907
+ console.log(formatSessionReport(result));
5908
+ }
5909
+ this.mode = "idle";
5910
+ this.isRunning = false;
5911
+ this.paperExecutor = null;
5912
+ }
5200
5913
  /**
5201
5914
  * Periodically check if the owner has approved the pending config hash.
5202
5915
  * Called from sendRelayStatus at most every 2.5 minutes (timestamp-throttled).
@@ -5267,6 +5980,29 @@ var AgentRuntime = class {
5267
5980
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
5268
5981
  } : void 0,
5269
5982
  positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0,
5983
+ paper: this.mode === "paper" && this.paperPortfolio ? (() => {
5984
+ const summary = this.paperPortfolio.getSummary(this.lastPrices);
5985
+ const trades = this.paperPortfolio.getTrades();
5986
+ let wins = 0, losses = 0;
5987
+ for (const t of trades) {
5988
+ if (t.action === "hold") continue;
5989
+ if (t.valueOutUSD >= t.valueInUSD) wins++;
5990
+ else losses++;
5991
+ }
5992
+ const curve = this.paperPortfolio.getEquityCurve();
5993
+ const equityCurve = curve.length > 50 ? curve.slice(-50) : curve;
5994
+ return {
5995
+ active: true,
5996
+ startedAt: summary.startedAt,
5997
+ simulatedValue: summary.currentValue,
5998
+ simulatedPnLPct: summary.totalReturnPct,
5999
+ tradeCount: summary.tradeCount,
6000
+ totalFees: summary.totalFees,
6001
+ wins,
6002
+ losses,
6003
+ equityCurve
6004
+ };
6005
+ })() : void 0,
5270
6006
  configHash: this.configHash || void 0,
5271
6007
  pendingConfigHash: this.pendingConfigHash
5272
6008
  // null preserved by JSON.stringify for clearing
@@ -5290,23 +6026,33 @@ var AgentRuntime = class {
5290
6026
  * Run a single trading cycle
5291
6027
  */
5292
6028
  async runCycle() {
6029
+ const isPaper = this.mode === "paper";
6030
+ const modeTag = isPaper ? "[PAPER] " : "";
5293
6031
  console.log(`
5294
- --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
6032
+ --- ${modeTag}Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
5295
6033
  this.cycleCount++;
5296
6034
  this.lastCycleAt = Date.now();
5297
6035
  const tokens = this.getTokensToTrack();
5298
6036
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
5299
- console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
6037
+ if (isPaper && this.paperPortfolio) {
6038
+ const simBalances = this.paperPortfolio.getBalances();
6039
+ marketData.balances = simBalances;
6040
+ marketData.portfolioValue = this.paperPortfolio.calculateValue(marketData.prices);
6041
+ this.paperExecutor?.updatePrices(marketData.prices);
6042
+ }
6043
+ console.log(`${modeTag}Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
5300
6044
  this.lastPortfolioValue = marketData.portfolioValue;
5301
6045
  this.lastPrices = marketData.prices;
5302
6046
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
5303
6047
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
5304
6048
  this.positionTracker.syncBalances(marketData.balances, marketData.prices);
5305
- const fundsOk = this.checkFundsLow(marketData);
5306
- if (!fundsOk) {
5307
- console.warn("Skipping trading cycle \u2014 ETH balance critically low");
5308
- this.sendRelayStatus();
5309
- return;
6049
+ if (!isPaper) {
6050
+ const fundsOk = this.checkFundsLow(marketData);
6051
+ if (!fundsOk) {
6052
+ console.warn("Skipping trading cycle \u2014 ETH balance critically low");
6053
+ this.sendRelayStatus();
6054
+ return;
6055
+ }
5310
6056
  }
5311
6057
  this.strategyContext.positions = this.positionTracker.getPositions();
5312
6058
  this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
@@ -5337,74 +6083,133 @@ var AgentRuntime = class {
5337
6083
  );
5338
6084
  }
5339
6085
  if (filteredSignals.length > 0) {
5340
- const vaultStatus = await this.vaultManager?.getVaultStatus();
5341
- if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
5342
- console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
5343
- }
5344
6086
  const preTradePortfolioValue = marketData.portfolioValue;
5345
- const results = await this.executor.executeAll(filteredSignals);
5346
- let totalFeesUSD = 0;
5347
- for (const result of results) {
5348
- if (result.success) {
5349
- console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
5350
- const feeCostBps = 20;
5351
- const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
5352
- const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
5353
- const feeCostUSD = amountUSD * feeCostBps / 1e4;
5354
- totalFeesUSD += feeCostUSD;
5355
- this.riskManager.updateFees(feeCostUSD);
5356
- this.relay?.sendMessage(
5357
- "trade_executed",
5358
- "success",
5359
- "Trade Executed",
5360
- `${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
5361
- {
5362
- action: result.signal.action,
5363
- txHash: result.txHash,
5364
- tokenIn: result.signal.tokenIn,
5365
- tokenOut: result.signal.tokenOut
5366
- }
5367
- );
5368
- } else {
5369
- console.warn(`Trade failed: ${result.error}`);
5370
- this.relay?.sendMessage(
5371
- "trade_failed",
5372
- "error",
5373
- "Trade Failed",
5374
- result.error || "Unknown error",
5375
- { action: result.signal.action }
5376
- );
6087
+ if (isPaper && this.paperExecutor && this.paperPortfolio) {
6088
+ const results = await this.paperExecutor.executeAll(filteredSignals);
6089
+ let totalFeesUSD = 0;
6090
+ for (const result of results) {
6091
+ if (result.success && result.paperFill) {
6092
+ this.paperPortfolio.applyFill(result.paperFill);
6093
+ totalFeesUSD += result.paperFill.feeUSD;
6094
+ this.riskManager.updateFees(result.paperFill.feeUSD);
6095
+ this.relay?.sendMessage(
6096
+ "paper_trade_executed",
6097
+ "success",
6098
+ "[PAPER] Trade Executed",
6099
+ `${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
6100
+ {
6101
+ action: result.signal.action,
6102
+ txHash: result.txHash,
6103
+ tokenIn: result.signal.tokenIn,
6104
+ tokenOut: result.signal.tokenOut,
6105
+ paper: true,
6106
+ valueInUSD: result.paperFill.valueInUSD,
6107
+ valueOutUSD: result.paperFill.valueOutUSD
6108
+ }
6109
+ );
6110
+ } else {
6111
+ this.relay?.sendMessage(
6112
+ "paper_trade_failed",
6113
+ "error",
6114
+ "[PAPER] Trade Failed",
6115
+ result.error || "Unknown error",
6116
+ { action: result.signal.action, paper: true }
6117
+ );
6118
+ }
6119
+ const tokenIn = result.signal.tokenIn.toLowerCase();
6120
+ const tokenOut = result.signal.tokenOut.toLowerCase();
6121
+ this.positionTracker.recordTrade({
6122
+ action: result.signal.action,
6123
+ tokenIn,
6124
+ tokenOut,
6125
+ amountIn: result.signal.amountIn,
6126
+ priceIn: marketData.prices[tokenIn] || 0,
6127
+ priceOut: marketData.prices[tokenOut] || 0,
6128
+ txHash: result.txHash,
6129
+ reasoning: result.signal.reasoning,
6130
+ success: result.success
6131
+ });
5377
6132
  }
6133
+ const postValue = this.paperPortfolio.calculateValue(marketData.prices);
6134
+ const marketPnL = postValue - preTradePortfolioValue + totalFeesUSD;
6135
+ this.riskManager.updatePnL(marketPnL);
6136
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
6137
+ if (marketPnL !== 0) {
6138
+ console.log(` [PAPER] Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
6139
+ }
6140
+ this.paperPortfolio.recordEquityPoint(marketData.prices);
6141
+ this.positionTracker.syncBalances(this.paperPortfolio.getBalances(), marketData.prices);
6142
+ this.lastPortfolioValue = postValue;
6143
+ this.paperPortfolio.save();
6144
+ } else {
6145
+ const vaultStatus = await this.vaultManager?.getVaultStatus();
6146
+ if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
6147
+ console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
6148
+ }
6149
+ const results = await this.executor.executeAll(filteredSignals);
6150
+ let totalFeesUSD = 0;
6151
+ for (const result of results) {
6152
+ if (result.success) {
6153
+ console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
6154
+ const feeCostBps = 20;
6155
+ const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
6156
+ const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
6157
+ const feeCostUSD = amountUSD * feeCostBps / 1e4;
6158
+ totalFeesUSD += feeCostUSD;
6159
+ this.riskManager.updateFees(feeCostUSD);
6160
+ this.relay?.sendMessage(
6161
+ "trade_executed",
6162
+ "success",
6163
+ "Trade Executed",
6164
+ `${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
6165
+ {
6166
+ action: result.signal.action,
6167
+ txHash: result.txHash,
6168
+ tokenIn: result.signal.tokenIn,
6169
+ tokenOut: result.signal.tokenOut
6170
+ }
6171
+ );
6172
+ } else {
6173
+ console.warn(`Trade failed: ${result.error}`);
6174
+ this.relay?.sendMessage(
6175
+ "trade_failed",
6176
+ "error",
6177
+ "Trade Failed",
6178
+ result.error || "Unknown error",
6179
+ { action: result.signal.action }
6180
+ );
6181
+ }
6182
+ }
6183
+ for (const result of results) {
6184
+ const tokenIn = result.signal.tokenIn.toLowerCase();
6185
+ const tokenOut = result.signal.tokenOut.toLowerCase();
6186
+ this.positionTracker.recordTrade({
6187
+ action: result.signal.action,
6188
+ tokenIn,
6189
+ tokenOut,
6190
+ amountIn: result.signal.amountIn,
6191
+ priceIn: marketData.prices[tokenIn] || 0,
6192
+ priceOut: marketData.prices[tokenOut] || 0,
6193
+ txHash: result.txHash,
6194
+ reasoning: result.signal.reasoning,
6195
+ success: result.success
6196
+ });
6197
+ }
6198
+ const postTokens = this.getTokensToTrack();
6199
+ const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
6200
+ const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
6201
+ this.riskManager.updatePnL(marketPnL);
6202
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
6203
+ if (marketPnL !== 0) {
6204
+ console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
6205
+ }
6206
+ this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
6207
+ this.lastPortfolioValue = postTradeData.portfolioValue;
6208
+ const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
6209
+ this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
5378
6210
  }
5379
- for (const result of results) {
5380
- const tokenIn = result.signal.tokenIn.toLowerCase();
5381
- const tokenOut = result.signal.tokenOut.toLowerCase();
5382
- this.positionTracker.recordTrade({
5383
- action: result.signal.action,
5384
- tokenIn,
5385
- tokenOut,
5386
- amountIn: result.signal.amountIn,
5387
- priceIn: marketData.prices[tokenIn] || 0,
5388
- priceOut: marketData.prices[tokenOut] || 0,
5389
- txHash: result.txHash,
5390
- reasoning: result.signal.reasoning,
5391
- success: result.success
5392
- });
5393
- }
5394
- const postTokens = this.getTokensToTrack();
5395
- const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
5396
- const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
5397
- this.riskManager.updatePnL(marketPnL);
5398
- this.positionTracker.saveRiskState(this.riskManager.exportState());
5399
- if (marketPnL !== 0) {
5400
- console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
5401
- }
5402
- this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
5403
- this.lastPortfolioValue = postTradeData.portfolioValue;
5404
- const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
5405
- this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
5406
6211
  }
5407
- if (this.perpConnected && this.perpTradingActive) {
6212
+ if (!isPaper && this.perpConnected && this.perpTradingActive) {
5408
6213
  try {
5409
6214
  await this.runPerpCycle();
5410
6215
  } catch (error) {
@@ -5719,8 +6524,8 @@ var AgentRuntime = class {
5719
6524
 
5720
6525
  // src/secure-env.ts
5721
6526
  var crypto = __toESM(require("crypto"));
5722
- var fs = __toESM(require("fs"));
5723
- var path = __toESM(require("path"));
6527
+ var fs3 = __toESM(require("fs"));
6528
+ var path3 = __toESM(require("path"));
5724
6529
  var ALGORITHM = "aes-256-gcm";
5725
6530
  var PBKDF2_ITERATIONS = 1e5;
5726
6531
  var SALT_LENGTH = 32;
@@ -5787,10 +6592,10 @@ function parseEnvFile(content) {
5787
6592
  return entries;
5788
6593
  }
5789
6594
  function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
5790
- if (!fs.existsSync(envPath)) {
6595
+ if (!fs3.existsSync(envPath)) {
5791
6596
  throw new Error(`File not found: ${envPath}`);
5792
6597
  }
5793
- const content = fs.readFileSync(envPath, "utf-8");
6598
+ const content = fs3.readFileSync(envPath, "utf-8");
5794
6599
  const entries = parseEnvFile(content);
5795
6600
  if (entries.length === 0) {
5796
6601
  throw new Error("No environment variables found in file");
@@ -5820,9 +6625,9 @@ function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
5820
6625
  entries: encryptedEntries
5821
6626
  };
5822
6627
  const encPath = envPath + ".enc";
5823
- fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
6628
+ fs3.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
5824
6629
  if (deleteOriginal) {
5825
- fs.unlinkSync(envPath);
6630
+ fs3.unlinkSync(envPath);
5826
6631
  }
5827
6632
  const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
5828
6633
  const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
@@ -5832,10 +6637,10 @@ function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
5832
6637
  return encPath;
5833
6638
  }
5834
6639
  function decryptEnvFile(encPath, passphrase) {
5835
- if (!fs.existsSync(encPath)) {
6640
+ if (!fs3.existsSync(encPath)) {
5836
6641
  throw new Error(`Encrypted env file not found: ${encPath}`);
5837
6642
  }
5838
- const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
6643
+ const content = JSON.parse(fs3.readFileSync(encPath, "utf-8"));
5839
6644
  if (content.version !== 1) {
5840
6645
  throw new Error(`Unsupported encrypted env version: ${content.version}`);
5841
6646
  }
@@ -5861,9 +6666,9 @@ function decryptEnvFile(encPath, passphrase) {
5861
6666
  return result;
5862
6667
  }
5863
6668
  function loadSecureEnv(basePath, passphrase) {
5864
- const encPath = path.join(basePath, ".env.enc");
5865
- const envPath = path.join(basePath, ".env");
5866
- if (fs.existsSync(encPath)) {
6669
+ const encPath = path3.join(basePath, ".env.enc");
6670
+ const envPath = path3.join(basePath, ".env");
6671
+ if (fs3.existsSync(encPath)) {
5867
6672
  if (!passphrase) {
5868
6673
  passphrase = process.env.EXAGENT_PASSPHRASE;
5869
6674
  }
@@ -5882,8 +6687,8 @@ function loadSecureEnv(basePath, passphrase) {
5882
6687
  return true;
5883
6688
  }
5884
6689
  }
5885
- if (fs.existsSync(envPath)) {
5886
- const content = fs.readFileSync(envPath, "utf-8");
6690
+ if (fs3.existsSync(envPath)) {
6691
+ const content = fs3.readFileSync(envPath, "utf-8");
5887
6692
  const entries = parseEnvFile(content);
5888
6693
  const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
5889
6694
  if (sensitiveKeys.length > 0) {
@@ -5902,7 +6707,7 @@ function loadSecureEnv(basePath, passphrase) {
5902
6707
  }
5903
6708
 
5904
6709
  // src/index.ts
5905
- var AGENT_VERSION = "0.1.38";
6710
+ var AGENT_VERSION = "0.1.39";
5906
6711
  // Annotate the CommonJS export names for ESM import in node:
5907
6712
  0 && (module.exports = {
5908
6713
  AGENT_VERSION,