@exagent/agent 0.1.37 → 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();
@@ -2248,6 +2248,24 @@ function classifyTradeError(message) {
2248
2248
  userMessage: "DEX aggregator not whitelisted on the router. Contact support."
2249
2249
  };
2250
2250
  }
2251
+ if (lower.includes("token approval reverted")) {
2252
+ return {
2253
+ category: "approval_failed",
2254
+ userMessage: "Token approval failed \u2014 this token may have non-standard approval behavior or be blocked. Skipping."
2255
+ };
2256
+ }
2257
+ if (lower.includes("exceeds allowance") || lower.includes("allowance")) {
2258
+ return {
2259
+ category: "allowance",
2260
+ userMessage: "Token allowance insufficient \u2014 approval may not have propagated. Will retry next cycle."
2261
+ };
2262
+ }
2263
+ if (lower.includes("swapfailed")) {
2264
+ return {
2265
+ category: "swap_failed",
2266
+ userMessage: "DEX swap failed \u2014 likely insufficient liquidity for this token. Will retry next cycle."
2267
+ };
2268
+ }
2251
2269
  if (lower.includes("reverted") || lower.includes("execution reverted")) {
2252
2270
  return {
2253
2271
  category: "reverted",
@@ -2274,8 +2292,11 @@ var RiskManager = class {
2274
2292
  lastResetDate = "";
2275
2293
  /** Minimum trade value in USD — trades below this are rejected as dust */
2276
2294
  minTradeValueUSD;
2277
- constructor(config) {
2295
+ /** Risk universe (0-4). Frontier (4) bypasses buy-only guardrails. */
2296
+ riskUniverse;
2297
+ constructor(config, riskUniverse = 0) {
2278
2298
  this.config = config;
2299
+ this.riskUniverse = riskUniverse;
2279
2300
  this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
2280
2301
  }
2281
2302
  /**
@@ -2311,6 +2332,9 @@ var RiskManager = class {
2311
2332
  if (signal.action === "sell") {
2312
2333
  return true;
2313
2334
  }
2335
+ if (this.riskUniverse === 4) {
2336
+ return true;
2337
+ }
2314
2338
  const signalValue = this.estimateSignalValue(signal, marketData);
2315
2339
  const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
2316
2340
  if (signalValue > maxPositionValue) {
@@ -2692,7 +2716,7 @@ var VaultManager = class {
2692
2716
  canCreateVault: false,
2693
2717
  cannotCreateReason: "Vault operations disabled (contract address not set)",
2694
2718
  requirementsMet: false,
2695
- requirements: { veXARequired: BigInt(0), isBypassed: false }
2719
+ requirements: { isBypassed: false }
2696
2720
  };
2697
2721
  }
2698
2722
  const vaultAddress = await this.getVaultAddress();
@@ -2729,16 +2753,10 @@ var VaultManager = class {
2729
2753
  }
2730
2754
  /**
2731
2755
  * Get vault creation requirements
2732
- * Note: No burnFee on mainnetvault creation requires USDC seed instead
2756
+ * Note: StakingStub always returns hasVaultAccess=trueno gate.
2733
2757
  */
2734
2758
  async getRequirements() {
2735
- const veXARequired = await this.publicClient.readContract({
2736
- address: this.addresses.vaultFactory,
2737
- abi: VAULT_FACTORY_ABI,
2738
- functionName: "minimumVeEXARequired"
2739
- });
2740
- const isBypassed = veXARequired === BigInt(0);
2741
- return { veXARequired, isBypassed };
2759
+ return { isBypassed: true };
2742
2760
  }
2743
2761
  /**
2744
2762
  * Get the agent's vault address (cached)
@@ -3126,6 +3144,609 @@ function openBrowser(url) {
3126
3144
  }
3127
3145
  }
3128
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
+
3129
3750
  // src/perp/client.ts
3130
3751
  var HyperliquidClient = class {
3131
3752
  apiUrl;
@@ -4395,6 +5016,11 @@ var AgentRuntime = class {
4395
5016
  allowedTokens = /* @__PURE__ */ new Set();
4396
5017
  strategyContext;
4397
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;
4398
5024
  // Perp trading components (null if perp not enabled)
4399
5025
  perpClient = null;
4400
5026
  perpSigner = null;
@@ -4417,9 +5043,13 @@ var AgentRuntime = class {
4417
5043
  cachedPerpMarginUsed = 0;
4418
5044
  cachedPerpLeverage = 0;
4419
5045
  cachedPerpOpenPositions = 0;
4420
- constructor(config) {
5046
+ constructor(config, options) {
4421
5047
  this.config = config;
5048
+ this.startInPaperMode = options?.paperMode ?? false;
5049
+ this._paperInitialBalances = options?.paperBalances;
4422
5050
  }
5051
+ /** Initial balances override for paper mode (from CLI --initial-eth/--initial-usdc) */
5052
+ _paperInitialBalances;
4423
5053
  /**
4424
5054
  * Initialize the agent runtime
4425
5055
  */
@@ -4451,7 +5081,7 @@ var AgentRuntime = class {
4451
5081
  };
4452
5082
  this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
4453
5083
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
4454
- this.riskManager = new RiskManager(this.config.trading);
5084
+ this.riskManager = new RiskManager(this.config.trading, this.riskUniverse);
4455
5085
  this.marketData = new MarketDataService(this.getRpcUrl(), store);
4456
5086
  setGlobalResolver(this.marketData.getResolver());
4457
5087
  const savedRisk = this.positionTracker.getRiskState();
@@ -4872,16 +5502,20 @@ var AgentRuntime = class {
4872
5502
  } catch (error) {
4873
5503
  console.warn("Initial balance sync failed (non-fatal):", error instanceof Error ? error.message : error);
4874
5504
  }
5505
+ if (this.startInPaperMode) {
5506
+ await this.startPaperTrading(this._paperInitialBalances);
5507
+ }
4875
5508
  this.sendRelayStatus();
4876
5509
  this.relay.sendMessage(
4877
5510
  "system",
4878
5511
  "success",
4879
5512
  "Agent Connected",
4880
- `${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"}.`,
4881
5514
  { wallet: this.client.address }
4882
5515
  );
4883
5516
  while (this.processAlive) {
4884
- if (this.mode === "trading" && this.isRunning) {
5517
+ const currentMode = this.mode;
5518
+ if ((currentMode === "trading" || currentMode === "paper") && this.isRunning) {
4885
5519
  try {
4886
5520
  await this.runCycle();
4887
5521
  } catch (error) {
@@ -4905,8 +5539,13 @@ var AgentRuntime = class {
4905
5539
  throw new Error("Agent is already running");
4906
5540
  }
4907
5541
  this.isRunning = true;
4908
- this.mode = "trading";
4909
- 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
+ }
4910
5549
  console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
4911
5550
  while (this.isRunning) {
4912
5551
  try {
@@ -4981,7 +5620,7 @@ var AgentRuntime = class {
4981
5620
  updated = true;
4982
5621
  }
4983
5622
  if (updated) {
4984
- this.riskManager = new RiskManager(this.config.trading);
5623
+ this.riskManager = new RiskManager(this.config.trading, this.riskUniverse);
4985
5624
  const savedRiskState = this.positionTracker.getRiskState();
4986
5625
  if (savedRiskState.lastResetDate) {
4987
5626
  this.riskManager.restoreState(savedRiskState);
@@ -5153,6 +5792,41 @@ var AgentRuntime = class {
5153
5792
  }
5154
5793
  break;
5155
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;
5156
5830
  case "refresh_status":
5157
5831
  this.sendRelayStatus();
5158
5832
  this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
@@ -5179,6 +5853,63 @@ var AgentRuntime = class {
5179
5853
  this.relay?.sendCommandResult(cmd.id, false, message);
5180
5854
  }
5181
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
+ }
5182
5913
  /**
5183
5914
  * Periodically check if the owner has approved the pending config hash.
5184
5915
  * Called from sendRelayStatus at most every 2.5 minutes (timestamp-throttled).
@@ -5249,6 +5980,29 @@ var AgentRuntime = class {
5249
5980
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
5250
5981
  } : void 0,
5251
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,
5252
6006
  configHash: this.configHash || void 0,
5253
6007
  pendingConfigHash: this.pendingConfigHash
5254
6008
  // null preserved by JSON.stringify for clearing
@@ -5272,23 +6026,33 @@ var AgentRuntime = class {
5272
6026
  * Run a single trading cycle
5273
6027
  */
5274
6028
  async runCycle() {
6029
+ const isPaper = this.mode === "paper";
6030
+ const modeTag = isPaper ? "[PAPER] " : "";
5275
6031
  console.log(`
5276
- --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
6032
+ --- ${modeTag}Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
5277
6033
  this.cycleCount++;
5278
6034
  this.lastCycleAt = Date.now();
5279
6035
  const tokens = this.getTokensToTrack();
5280
6036
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
5281
- 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)}`);
5282
6044
  this.lastPortfolioValue = marketData.portfolioValue;
5283
6045
  this.lastPrices = marketData.prices;
5284
6046
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
5285
6047
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
5286
6048
  this.positionTracker.syncBalances(marketData.balances, marketData.prices);
5287
- const fundsOk = this.checkFundsLow(marketData);
5288
- if (!fundsOk) {
5289
- console.warn("Skipping trading cycle \u2014 ETH balance critically low");
5290
- this.sendRelayStatus();
5291
- 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
+ }
5292
6056
  }
5293
6057
  this.strategyContext.positions = this.positionTracker.getPositions();
5294
6058
  this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
@@ -5319,74 +6083,133 @@ var AgentRuntime = class {
5319
6083
  );
5320
6084
  }
5321
6085
  if (filteredSignals.length > 0) {
5322
- const vaultStatus = await this.vaultManager?.getVaultStatus();
5323
- if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
5324
- console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
5325
- }
5326
6086
  const preTradePortfolioValue = marketData.portfolioValue;
5327
- const results = await this.executor.executeAll(filteredSignals);
5328
- let totalFeesUSD = 0;
5329
- for (const result of results) {
5330
- if (result.success) {
5331
- console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
5332
- const feeCostBps = 20;
5333
- const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
5334
- const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
5335
- const feeCostUSD = amountUSD * feeCostBps / 1e4;
5336
- totalFeesUSD += feeCostUSD;
5337
- this.riskManager.updateFees(feeCostUSD);
5338
- this.relay?.sendMessage(
5339
- "trade_executed",
5340
- "success",
5341
- "Trade Executed",
5342
- `${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
5343
- {
5344
- action: result.signal.action,
5345
- txHash: result.txHash,
5346
- tokenIn: result.signal.tokenIn,
5347
- tokenOut: result.signal.tokenOut
5348
- }
5349
- );
5350
- } else {
5351
- console.warn(`Trade failed: ${result.error}`);
5352
- this.relay?.sendMessage(
5353
- "trade_failed",
5354
- "error",
5355
- "Trade Failed",
5356
- result.error || "Unknown error",
5357
- { action: result.signal.action }
5358
- );
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
+ });
5359
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);
5360
6210
  }
5361
- for (const result of results) {
5362
- const tokenIn = result.signal.tokenIn.toLowerCase();
5363
- const tokenOut = result.signal.tokenOut.toLowerCase();
5364
- this.positionTracker.recordTrade({
5365
- action: result.signal.action,
5366
- tokenIn,
5367
- tokenOut,
5368
- amountIn: result.signal.amountIn,
5369
- priceIn: marketData.prices[tokenIn] || 0,
5370
- priceOut: marketData.prices[tokenOut] || 0,
5371
- txHash: result.txHash,
5372
- reasoning: result.signal.reasoning,
5373
- success: result.success
5374
- });
5375
- }
5376
- const postTokens = this.getTokensToTrack();
5377
- const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
5378
- const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
5379
- this.riskManager.updatePnL(marketPnL);
5380
- this.positionTracker.saveRiskState(this.riskManager.exportState());
5381
- if (marketPnL !== 0) {
5382
- console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
5383
- }
5384
- this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
5385
- this.lastPortfolioValue = postTradeData.portfolioValue;
5386
- const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
5387
- this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
5388
6211
  }
5389
- if (this.perpConnected && this.perpTradingActive) {
6212
+ if (!isPaper && this.perpConnected && this.perpTradingActive) {
5390
6213
  try {
5391
6214
  await this.runPerpCycle();
5392
6215
  } catch (error) {
@@ -5701,8 +6524,8 @@ var AgentRuntime = class {
5701
6524
 
5702
6525
  // src/secure-env.ts
5703
6526
  var crypto = __toESM(require("crypto"));
5704
- var fs = __toESM(require("fs"));
5705
- var path = __toESM(require("path"));
6527
+ var fs3 = __toESM(require("fs"));
6528
+ var path3 = __toESM(require("path"));
5706
6529
  var ALGORITHM = "aes-256-gcm";
5707
6530
  var PBKDF2_ITERATIONS = 1e5;
5708
6531
  var SALT_LENGTH = 32;
@@ -5769,10 +6592,10 @@ function parseEnvFile(content) {
5769
6592
  return entries;
5770
6593
  }
5771
6594
  function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
5772
- if (!fs.existsSync(envPath)) {
6595
+ if (!fs3.existsSync(envPath)) {
5773
6596
  throw new Error(`File not found: ${envPath}`);
5774
6597
  }
5775
- const content = fs.readFileSync(envPath, "utf-8");
6598
+ const content = fs3.readFileSync(envPath, "utf-8");
5776
6599
  const entries = parseEnvFile(content);
5777
6600
  if (entries.length === 0) {
5778
6601
  throw new Error("No environment variables found in file");
@@ -5802,9 +6625,9 @@ function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
5802
6625
  entries: encryptedEntries
5803
6626
  };
5804
6627
  const encPath = envPath + ".enc";
5805
- fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
6628
+ fs3.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
5806
6629
  if (deleteOriginal) {
5807
- fs.unlinkSync(envPath);
6630
+ fs3.unlinkSync(envPath);
5808
6631
  }
5809
6632
  const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
5810
6633
  const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
@@ -5814,10 +6637,10 @@ function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
5814
6637
  return encPath;
5815
6638
  }
5816
6639
  function decryptEnvFile(encPath, passphrase) {
5817
- if (!fs.existsSync(encPath)) {
6640
+ if (!fs3.existsSync(encPath)) {
5818
6641
  throw new Error(`Encrypted env file not found: ${encPath}`);
5819
6642
  }
5820
- const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
6643
+ const content = JSON.parse(fs3.readFileSync(encPath, "utf-8"));
5821
6644
  if (content.version !== 1) {
5822
6645
  throw new Error(`Unsupported encrypted env version: ${content.version}`);
5823
6646
  }
@@ -5843,9 +6666,9 @@ function decryptEnvFile(encPath, passphrase) {
5843
6666
  return result;
5844
6667
  }
5845
6668
  function loadSecureEnv(basePath, passphrase) {
5846
- const encPath = path.join(basePath, ".env.enc");
5847
- const envPath = path.join(basePath, ".env");
5848
- if (fs.existsSync(encPath)) {
6669
+ const encPath = path3.join(basePath, ".env.enc");
6670
+ const envPath = path3.join(basePath, ".env");
6671
+ if (fs3.existsSync(encPath)) {
5849
6672
  if (!passphrase) {
5850
6673
  passphrase = process.env.EXAGENT_PASSPHRASE;
5851
6674
  }
@@ -5864,8 +6687,8 @@ function loadSecureEnv(basePath, passphrase) {
5864
6687
  return true;
5865
6688
  }
5866
6689
  }
5867
- if (fs.existsSync(envPath)) {
5868
- const content = fs.readFileSync(envPath, "utf-8");
6690
+ if (fs3.existsSync(envPath)) {
6691
+ const content = fs3.readFileSync(envPath, "utf-8");
5869
6692
  const entries = parseEnvFile(content);
5870
6693
  const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
5871
6694
  if (sensitiveKeys.length > 0) {
@@ -5884,7 +6707,7 @@ function loadSecureEnv(basePath, passphrase) {
5884
6707
  }
5885
6708
 
5886
6709
  // src/index.ts
5887
- var AGENT_VERSION = "0.1.37";
6710
+ var AGENT_VERSION = "0.1.39";
5888
6711
  // Annotate the CommonJS export names for ESM import in node:
5889
6712
  0 && (module.exports = {
5890
6713
  AGENT_VERSION,