@exagent/agent 0.1.38 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3AJR5TQF.mjs +6499 -0
- package/dist/chunk-6Y4PBQ2U.mjs +6806 -0
- package/dist/chunk-EPXZ6MEW.mjs +6959 -0
- package/dist/chunk-KQUNPLRE.mjs +6959 -0
- package/dist/chunk-LDTWZMEI.mjs +6818 -0
- package/dist/cli.js +4123 -2045
- package/dist/cli.mjs +1086 -21
- package/dist/index.d.mts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +898 -93
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
4927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
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
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
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
|
|
5723
|
-
var
|
|
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 (!
|
|
6595
|
+
if (!fs3.existsSync(envPath)) {
|
|
5791
6596
|
throw new Error(`File not found: ${envPath}`);
|
|
5792
6597
|
}
|
|
5793
|
-
const content =
|
|
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
|
-
|
|
6628
|
+
fs3.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
5824
6629
|
if (deleteOriginal) {
|
|
5825
|
-
|
|
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 (!
|
|
6640
|
+
if (!fs3.existsSync(encPath)) {
|
|
5836
6641
|
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
5837
6642
|
}
|
|
5838
|
-
const content = JSON.parse(
|
|
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 =
|
|
5865
|
-
const envPath =
|
|
5866
|
-
if (
|
|
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 (
|
|
5886
|
-
const content =
|
|
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.
|
|
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,
|