@exagent/agent 0.1.20 → 0.1.22
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-EJHDRG5Y.mjs +5305 -0
- package/dist/chunk-N6RIRCGH.mjs +5221 -0
- package/dist/chunk-NFE6HTL3.mjs +5218 -0
- package/dist/chunk-NIZP5EVK.mjs +5226 -0
- package/dist/chunk-U6YJHCO3.mjs +5226 -0
- package/dist/cli.js +1522 -1120
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +204 -6
- package/dist/index.d.ts +204 -6
- package/dist/index.js +976 -570
- package/dist/index.mjs +5 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AGENT_VERSION: () => AGENT_VERSION,
|
|
33
34
|
AgentConfigSchema: () => AgentConfigSchema,
|
|
34
35
|
AgentRuntime: () => AgentRuntime,
|
|
35
36
|
AnthropicAdapter: () => AnthropicAdapter,
|
|
@@ -53,6 +54,7 @@ __export(index_exports, {
|
|
|
53
54
|
PerpOnboarding: () => PerpOnboarding,
|
|
54
55
|
PerpTradeRecorder: () => PerpTradeRecorder,
|
|
55
56
|
PositionManager: () => PositionManager,
|
|
57
|
+
PositionTracker: () => PositionTracker,
|
|
56
58
|
RelayClient: () => RelayClient,
|
|
57
59
|
RelayConfigSchema: () => RelayConfigSchema,
|
|
58
60
|
RiskManager: () => RiskManager,
|
|
@@ -82,105 +84,641 @@ __export(index_exports, {
|
|
|
82
84
|
module.exports = __toCommonJS(index_exports);
|
|
83
85
|
|
|
84
86
|
// src/runtime.ts
|
|
85
|
-
var
|
|
87
|
+
var import_sdk = require("@exagent/sdk");
|
|
86
88
|
var import_viem6 = require("viem");
|
|
87
89
|
var import_chains4 = require("viem/chains");
|
|
88
90
|
var import_accounts5 = require("viem/accounts");
|
|
89
91
|
|
|
90
|
-
// src/
|
|
91
|
-
var
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
// src/trading/market.ts
|
|
93
|
+
var import_viem = require("viem");
|
|
94
|
+
var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
95
|
+
var TOKEN_DECIMALS = {
|
|
96
|
+
// Base Mainnet — Core tokens
|
|
97
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
98
|
+
// USDC
|
|
99
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
100
|
+
// USDbC
|
|
101
|
+
"0x4200000000000000000000000000000000000006": 18,
|
|
102
|
+
// WETH
|
|
103
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
104
|
+
// DAI
|
|
105
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
106
|
+
// cbETH
|
|
107
|
+
[NATIVE_ETH.toLowerCase()]: 18,
|
|
108
|
+
// Native ETH
|
|
109
|
+
// Base Mainnet — Established tokens
|
|
110
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
111
|
+
// AERO (Aerodrome)
|
|
112
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
113
|
+
// BRETT
|
|
114
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
115
|
+
// DEGEN
|
|
116
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
117
|
+
// VIRTUAL
|
|
118
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
119
|
+
// TOSHI
|
|
120
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
121
|
+
// cbBTC
|
|
122
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
123
|
+
// ezETH (Renzo)
|
|
124
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
125
|
+
// wstETH (Lido)
|
|
126
|
+
};
|
|
127
|
+
function getTokenDecimals(address) {
|
|
128
|
+
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
129
|
+
if (decimals === void 0) {
|
|
130
|
+
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
131
|
+
return 18;
|
|
98
132
|
}
|
|
99
|
-
|
|
133
|
+
return decimals;
|
|
134
|
+
}
|
|
135
|
+
var TOKEN_TO_COINGECKO = {
|
|
136
|
+
// Core
|
|
137
|
+
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
138
|
+
// WETH
|
|
139
|
+
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
140
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
141
|
+
// USDC
|
|
142
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
143
|
+
// USDbC
|
|
144
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
145
|
+
// cbETH
|
|
146
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
147
|
+
// DAI
|
|
148
|
+
// Established
|
|
149
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
150
|
+
// AERO
|
|
151
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
152
|
+
// BRETT
|
|
153
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
154
|
+
// DEGEN
|
|
155
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
156
|
+
// VIRTUAL
|
|
157
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
158
|
+
// TOSHI
|
|
159
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
160
|
+
// cbBTC
|
|
161
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
162
|
+
// ezETH
|
|
163
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
164
|
+
// wstETH
|
|
165
|
+
};
|
|
166
|
+
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
167
|
+
var PRICE_STALENESS_MS = 6e4;
|
|
168
|
+
var MarketDataService = class {
|
|
169
|
+
rpcUrl;
|
|
170
|
+
client;
|
|
171
|
+
/** Cached prices from last fetch */
|
|
172
|
+
cachedPrices = {};
|
|
173
|
+
/** Timestamp of last successful price fetch */
|
|
174
|
+
lastPriceFetchAt = 0;
|
|
175
|
+
constructor(rpcUrl) {
|
|
176
|
+
this.rpcUrl = rpcUrl;
|
|
177
|
+
this.client = (0, import_viem.createPublicClient)({
|
|
178
|
+
transport: (0, import_viem.http)(rpcUrl)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Cached volume data */
|
|
182
|
+
cachedVolume24h = {};
|
|
183
|
+
/** Cached price change data */
|
|
184
|
+
cachedPriceChange24h = {};
|
|
185
|
+
/**
|
|
186
|
+
* Fetch current market data for the agent
|
|
187
|
+
*/
|
|
188
|
+
async fetchMarketData(walletAddress, tokenAddresses) {
|
|
189
|
+
const prices = await this.fetchPrices(tokenAddresses);
|
|
190
|
+
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
191
|
+
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
192
|
+
let gasPrice;
|
|
193
|
+
try {
|
|
194
|
+
gasPrice = await this.client.getGasPrice();
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
100
197
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
prices,
|
|
200
|
+
balances,
|
|
201
|
+
portfolioValue,
|
|
202
|
+
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
203
|
+
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
204
|
+
gasPrice,
|
|
205
|
+
network: {
|
|
206
|
+
chainId: this.client.chain?.id ?? 8453
|
|
207
|
+
}
|
|
104
208
|
};
|
|
105
209
|
}
|
|
106
210
|
/**
|
|
107
|
-
*
|
|
211
|
+
* Check if cached prices are still fresh
|
|
108
212
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return `Local (${this.config.model || "ollama"})`;
|
|
112
|
-
}
|
|
113
|
-
return this.config.model || this.config.provider;
|
|
213
|
+
get pricesAreFresh() {
|
|
214
|
+
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
114
215
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Fetch token prices from CoinGecko free API
|
|
218
|
+
* Returns cached prices if still fresh (<60s old)
|
|
219
|
+
*/
|
|
220
|
+
async fetchPrices(tokenAddresses) {
|
|
221
|
+
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
222
|
+
const prices2 = { ...this.cachedPrices };
|
|
223
|
+
for (const addr of tokenAddresses) {
|
|
224
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
225
|
+
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
226
|
+
prices2[addr.toLowerCase()] = 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return prices2;
|
|
124
230
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const response = await this.client.chat.completions.create({
|
|
133
|
-
model: this.config.model || "gpt-4.1",
|
|
134
|
-
messages: messages.map((m) => ({
|
|
135
|
-
role: m.role,
|
|
136
|
-
content: m.content
|
|
137
|
-
})),
|
|
138
|
-
temperature: this.config.temperature,
|
|
139
|
-
max_tokens: this.config.maxTokens
|
|
140
|
-
});
|
|
141
|
-
const choice = response.choices[0];
|
|
142
|
-
if (!choice || !choice.message) {
|
|
143
|
-
throw new Error("No response from OpenAI");
|
|
231
|
+
const prices = {};
|
|
232
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
233
|
+
for (const addr of tokenAddresses) {
|
|
234
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
235
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
236
|
+
idsToFetch.add(cgId);
|
|
144
237
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
238
|
+
}
|
|
239
|
+
idsToFetch.add("ethereum");
|
|
240
|
+
if (idsToFetch.size > 0) {
|
|
241
|
+
try {
|
|
242
|
+
const ids = Array.from(idsToFetch).join(",");
|
|
243
|
+
const response = await fetch(
|
|
244
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
245
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
246
|
+
);
|
|
247
|
+
if (response.ok) {
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
for (const [cgId, priceData] of Object.entries(data)) {
|
|
250
|
+
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
251
|
+
if (id === cgId) {
|
|
252
|
+
const key = addr.toLowerCase();
|
|
253
|
+
prices[key] = priceData.usd;
|
|
254
|
+
if (priceData.usd_24h_vol !== void 0) {
|
|
255
|
+
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
256
|
+
}
|
|
257
|
+
if (priceData.usd_24h_change !== void 0) {
|
|
258
|
+
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.lastPriceFetchAt = Date.now();
|
|
264
|
+
} else {
|
|
265
|
+
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
156
269
|
}
|
|
157
|
-
throw error;
|
|
158
270
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
apiKey;
|
|
165
|
-
baseUrl;
|
|
166
|
-
constructor(config) {
|
|
167
|
-
super(config);
|
|
168
|
-
if (!config.apiKey) {
|
|
169
|
-
throw new Error("Anthropic API key required");
|
|
271
|
+
for (const addr of tokenAddresses) {
|
|
272
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
273
|
+
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
274
|
+
prices[addr.toLowerCase()] = 1;
|
|
275
|
+
}
|
|
170
276
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
277
|
+
const missingAddrs = tokenAddresses.filter(
|
|
278
|
+
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
279
|
+
);
|
|
280
|
+
if (missingAddrs.length > 0) {
|
|
281
|
+
try {
|
|
282
|
+
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
283
|
+
const llamaResponse = await fetch(
|
|
284
|
+
`https://coins.llama.fi/prices/current/${coins}`,
|
|
285
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
286
|
+
);
|
|
287
|
+
if (llamaResponse.ok) {
|
|
288
|
+
const llamaData = await llamaResponse.json();
|
|
289
|
+
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
290
|
+
const addr = key.replace("base:", "").toLowerCase();
|
|
291
|
+
if (data.price && data.confidence > 0.5) {
|
|
292
|
+
prices[addr] = data.price;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (Object.keys(prices).length > 0) {
|
|
301
|
+
this.cachedPrices = prices;
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
304
|
+
console.warn("Using cached prices (last successful fetch was stale)");
|
|
305
|
+
return { ...this.cachedPrices };
|
|
306
|
+
}
|
|
307
|
+
for (const addr of tokenAddresses) {
|
|
308
|
+
if (!prices[addr.toLowerCase()]) {
|
|
309
|
+
console.warn(`No price available for ${addr}, using 0`);
|
|
310
|
+
prices[addr.toLowerCase()] = 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return prices;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Fetch real on-chain balances: native ETH + ERC-20 tokens
|
|
317
|
+
*/
|
|
318
|
+
async fetchBalances(walletAddress, tokenAddresses) {
|
|
319
|
+
const balances = {};
|
|
320
|
+
const wallet = walletAddress;
|
|
321
|
+
try {
|
|
322
|
+
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
323
|
+
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
324
|
+
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
325
|
+
try {
|
|
326
|
+
const balance = await this.client.readContract({
|
|
327
|
+
address: tokenAddress,
|
|
328
|
+
abi: import_viem.erc20Abi,
|
|
329
|
+
functionName: "balanceOf",
|
|
330
|
+
args: [wallet]
|
|
331
|
+
});
|
|
332
|
+
return { address: tokenAddress.toLowerCase(), balance };
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
const results = await Promise.all(erc20Promises);
|
|
338
|
+
for (const { address, balance } of results) {
|
|
339
|
+
balances[address] = balance;
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
343
|
+
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
344
|
+
for (const address of tokenAddresses) {
|
|
345
|
+
balances[address.toLowerCase()] = 0n;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return balances;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Calculate total portfolio value in USD
|
|
352
|
+
*/
|
|
353
|
+
calculatePortfolioValue(balances, prices) {
|
|
354
|
+
let total = 0;
|
|
355
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
356
|
+
const price = prices[address.toLowerCase()] || 0;
|
|
357
|
+
const decimals = getTokenDecimals(address);
|
|
358
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
359
|
+
total += amount * price;
|
|
360
|
+
}
|
|
361
|
+
return total;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/position-tracker.ts
|
|
366
|
+
var BASE_ASSETS = /* @__PURE__ */ new Set([
|
|
367
|
+
NATIVE_ETH.toLowerCase(),
|
|
368
|
+
// Native ETH
|
|
369
|
+
"0x4200000000000000000000000000000000000006",
|
|
370
|
+
// WETH
|
|
371
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
372
|
+
// USDC
|
|
373
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
|
|
374
|
+
// USDbC
|
|
375
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
|
|
376
|
+
// DAI
|
|
377
|
+
"0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
|
|
378
|
+
// USDT
|
|
379
|
+
"0x60a3e35cc302bfa44cb36dc100b2587cd09b9c83"
|
|
380
|
+
// EURC
|
|
381
|
+
]);
|
|
382
|
+
var TOKEN_SYMBOLS = {
|
|
383
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "AERO",
|
|
384
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "BRETT",
|
|
385
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "DEGEN",
|
|
386
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "VIRTUAL",
|
|
387
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "TOSHI",
|
|
388
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "cbBTC",
|
|
389
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "ezETH",
|
|
390
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wstETH",
|
|
391
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "cbETH",
|
|
392
|
+
"0x13403fb738c97cf7564f279288468c140aaed05c": "EXA"
|
|
393
|
+
};
|
|
394
|
+
var KEY_POSITIONS = "__positions";
|
|
395
|
+
var KEY_TRADE_HISTORY = "__trade_history";
|
|
396
|
+
var KEY_RISK_STATE = "__risk_state";
|
|
397
|
+
var PositionTracker = class {
|
|
398
|
+
store;
|
|
399
|
+
positions;
|
|
400
|
+
tradeHistory;
|
|
401
|
+
maxTradeHistory;
|
|
402
|
+
constructor(store, options) {
|
|
403
|
+
this.store = store;
|
|
404
|
+
this.maxTradeHistory = options?.maxTradeHistory ?? 50;
|
|
405
|
+
this.positions = store.get(KEY_POSITIONS) || {};
|
|
406
|
+
this.tradeHistory = store.get(KEY_TRADE_HISTORY) || [];
|
|
407
|
+
const posCount = Object.keys(this.positions).length;
|
|
408
|
+
if (posCount > 0 || this.tradeHistory.length > 0) {
|
|
409
|
+
console.log(`Position tracker loaded: ${posCount} positions, ${this.tradeHistory.length} trade records`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// ============================================================
|
|
413
|
+
// TRADE RECORDING (called by runtime after execution)
|
|
414
|
+
// ============================================================
|
|
415
|
+
/**
|
|
416
|
+
* Record a trade result. On buy: creates/updates position with cost-basis
|
|
417
|
+
* weighted average. On sell: calculates realized PnL and removes if fully sold.
|
|
418
|
+
*/
|
|
419
|
+
recordTrade(params) {
|
|
420
|
+
const { action, tokenIn, tokenOut, amountIn, priceIn, priceOut, txHash, reasoning, success } = params;
|
|
421
|
+
const decimalsIn = getTokenDecimals(tokenIn);
|
|
422
|
+
const amountInUnits = Number(amountIn) / Math.pow(10, decimalsIn);
|
|
423
|
+
const tradeValueUSD = amountInUnits * priceIn;
|
|
424
|
+
const record = {
|
|
425
|
+
timestamp: Date.now(),
|
|
426
|
+
action,
|
|
427
|
+
tokenIn: tokenIn.toLowerCase(),
|
|
428
|
+
tokenOut: tokenOut.toLowerCase(),
|
|
429
|
+
amountIn: amountIn.toString(),
|
|
430
|
+
priceUSD: tradeValueUSD,
|
|
431
|
+
txHash,
|
|
432
|
+
reasoning,
|
|
433
|
+
success
|
|
434
|
+
};
|
|
435
|
+
if (success) {
|
|
436
|
+
if (action === "buy") {
|
|
437
|
+
this.handleBuy(tokenOut.toLowerCase(), tradeValueUSD, priceOut, txHash);
|
|
438
|
+
} else if (action === "sell") {
|
|
439
|
+
const realizedPnL = this.handleSell(tokenIn.toLowerCase(), tradeValueUSD);
|
|
440
|
+
record.realizedPnL = realizedPnL;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this.tradeHistory.unshift(record);
|
|
444
|
+
if (this.tradeHistory.length > this.maxTradeHistory) {
|
|
445
|
+
this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
|
|
446
|
+
}
|
|
447
|
+
this.persist();
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Handle a buy: create or update position with cost-basis weighted average.
|
|
451
|
+
*/
|
|
452
|
+
handleBuy(token, costUSD, priceUSD, txHash) {
|
|
453
|
+
if (BASE_ASSETS.has(token) || priceUSD <= 0 || costUSD <= 0) return;
|
|
454
|
+
const existing = this.positions[token];
|
|
455
|
+
const acquiredAmount = costUSD / priceUSD;
|
|
456
|
+
if (existing) {
|
|
457
|
+
const newTotalCost = existing.totalCostBasis + costUSD;
|
|
458
|
+
const newTotalAmount = existing.totalAmountAcquired + acquiredAmount;
|
|
459
|
+
existing.averageEntryPrice = newTotalAmount > 0 ? newTotalCost / newTotalAmount : priceUSD;
|
|
460
|
+
existing.totalCostBasis = newTotalCost;
|
|
461
|
+
existing.totalAmountAcquired = newTotalAmount;
|
|
462
|
+
existing.lastUpdateTimestamp = Date.now();
|
|
463
|
+
if (txHash) {
|
|
464
|
+
existing.txHashes.push(txHash);
|
|
465
|
+
if (existing.txHashes.length > 10) existing.txHashes.shift();
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
this.positions[token] = {
|
|
469
|
+
token,
|
|
470
|
+
symbol: TOKEN_SYMBOLS[token],
|
|
471
|
+
entryPrice: priceUSD,
|
|
472
|
+
averageEntryPrice: priceUSD,
|
|
473
|
+
totalCostBasis: costUSD,
|
|
474
|
+
totalAmountAcquired: acquiredAmount,
|
|
475
|
+
currentAmount: acquiredAmount,
|
|
476
|
+
entryTimestamp: Date.now(),
|
|
477
|
+
lastUpdateTimestamp: Date.now(),
|
|
478
|
+
txHashes: txHash ? [txHash] : []
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Handle a sell: calculate realized PnL and remove position if fully sold.
|
|
484
|
+
* Returns the realized PnL in USD.
|
|
485
|
+
*/
|
|
486
|
+
handleSell(token, saleValueUSD) {
|
|
487
|
+
if (BASE_ASSETS.has(token)) return 0;
|
|
488
|
+
const position = this.positions[token];
|
|
489
|
+
if (!position || position.averageEntryPrice <= 0) return 0;
|
|
490
|
+
const estimatedUnitsSold = position.currentAmount > 0 ? Math.min(position.currentAmount, saleValueUSD / position.averageEntryPrice) : saleValueUSD / position.averageEntryPrice;
|
|
491
|
+
const costBasisOfSold = estimatedUnitsSold * position.averageEntryPrice;
|
|
492
|
+
const realizedPnL = saleValueUSD - costBasisOfSold;
|
|
493
|
+
position.totalAmountAcquired = Math.max(0, position.totalAmountAcquired - estimatedUnitsSold);
|
|
494
|
+
position.totalCostBasis = Math.max(0, position.totalCostBasis - costBasisOfSold);
|
|
495
|
+
position.lastUpdateTimestamp = Date.now();
|
|
496
|
+
return realizedPnL;
|
|
497
|
+
}
|
|
498
|
+
// ============================================================
|
|
499
|
+
// BALANCE SYNC (called by runtime each cycle)
|
|
500
|
+
// ============================================================
|
|
501
|
+
/**
|
|
502
|
+
* Sync tracked positions with on-chain balances.
|
|
503
|
+
* Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
|
|
504
|
+
*/
|
|
505
|
+
syncBalances(balances, prices) {
|
|
506
|
+
let changed = false;
|
|
507
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
508
|
+
const token = address.toLowerCase();
|
|
509
|
+
if (BASE_ASSETS.has(token)) continue;
|
|
510
|
+
const decimals = getTokenDecimals(token);
|
|
511
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
512
|
+
if (amount > 0) {
|
|
513
|
+
if (this.positions[token]) {
|
|
514
|
+
if (this.positions[token].currentAmount !== amount) {
|
|
515
|
+
this.positions[token].currentAmount = amount;
|
|
516
|
+
this.positions[token].lastUpdateTimestamp = Date.now();
|
|
517
|
+
changed = true;
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
const price = prices[token] || 0;
|
|
521
|
+
this.positions[token] = {
|
|
522
|
+
token,
|
|
523
|
+
symbol: TOKEN_SYMBOLS[token],
|
|
524
|
+
entryPrice: price,
|
|
525
|
+
averageEntryPrice: price,
|
|
526
|
+
totalCostBasis: amount * price,
|
|
527
|
+
totalAmountAcquired: amount,
|
|
528
|
+
currentAmount: amount,
|
|
529
|
+
entryTimestamp: Date.now(),
|
|
530
|
+
lastUpdateTimestamp: Date.now(),
|
|
531
|
+
txHashes: []
|
|
532
|
+
};
|
|
533
|
+
if (price > 0) {
|
|
534
|
+
console.log(`Position tracker: detected new holding ${TOKEN_SYMBOLS[token] || token.slice(0, 10)} (${amount.toFixed(4)} units @ $${price.toFixed(4)})`);
|
|
535
|
+
}
|
|
536
|
+
changed = true;
|
|
537
|
+
}
|
|
538
|
+
} else if (this.positions[token]) {
|
|
539
|
+
delete this.positions[token];
|
|
540
|
+
changed = true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (changed) {
|
|
544
|
+
this.persist();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// ============================================================
|
|
548
|
+
// QUERY METHODS (for strategies)
|
|
549
|
+
// ============================================================
|
|
550
|
+
/** Get all tracked positions */
|
|
551
|
+
getPositions() {
|
|
552
|
+
return Object.values(this.positions);
|
|
553
|
+
}
|
|
554
|
+
/** Get a single position by token address */
|
|
555
|
+
getPosition(token) {
|
|
556
|
+
return this.positions[token.toLowerCase()];
|
|
557
|
+
}
|
|
558
|
+
/** Get trade history (newest first) */
|
|
559
|
+
getTradeHistory(limit) {
|
|
560
|
+
return limit ? this.tradeHistory.slice(0, limit) : [...this.tradeHistory];
|
|
561
|
+
}
|
|
562
|
+
/** Get unrealized PnL per position given current prices */
|
|
563
|
+
getUnrealizedPnL(prices) {
|
|
564
|
+
const pnl = {};
|
|
565
|
+
for (const pos of Object.values(this.positions)) {
|
|
566
|
+
const currentPrice = prices[pos.token] || 0;
|
|
567
|
+
if (currentPrice > 0 && pos.averageEntryPrice > 0 && pos.currentAmount > 0) {
|
|
568
|
+
const currentValue = pos.currentAmount * currentPrice;
|
|
569
|
+
const costBasis = pos.currentAmount * pos.averageEntryPrice;
|
|
570
|
+
pnl[pos.token] = currentValue - costBasis;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return pnl;
|
|
574
|
+
}
|
|
575
|
+
/** Get total unrealized PnL across all positions */
|
|
576
|
+
getTotalUnrealizedPnL(prices) {
|
|
577
|
+
const pnl = this.getUnrealizedPnL(prices);
|
|
578
|
+
return Object.values(pnl).reduce((sum, v) => sum + v, 0);
|
|
579
|
+
}
|
|
580
|
+
// ============================================================
|
|
581
|
+
// RISK STATE PERSISTENCE
|
|
582
|
+
// ============================================================
|
|
583
|
+
/** Load persisted risk state */
|
|
584
|
+
getRiskState() {
|
|
585
|
+
return this.store.get(KEY_RISK_STATE) || {
|
|
586
|
+
dailyPnL: 0,
|
|
587
|
+
dailyFees: 0,
|
|
588
|
+
lastResetDate: ""
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/** Save risk state to persistent store */
|
|
592
|
+
saveRiskState(state) {
|
|
593
|
+
this.store.set(KEY_RISK_STATE, state);
|
|
594
|
+
}
|
|
595
|
+
// ============================================================
|
|
596
|
+
// RELAY SUMMARY
|
|
597
|
+
// ============================================================
|
|
598
|
+
/** Get a compact summary for relay heartbeats */
|
|
599
|
+
getPositionSummary(prices) {
|
|
600
|
+
const unrealizedPnL = this.getUnrealizedPnL(prices);
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
const topPositions = Object.values(this.positions).map((pos) => ({
|
|
603
|
+
token: pos.token,
|
|
604
|
+
symbol: pos.symbol,
|
|
605
|
+
unrealizedPnL: unrealizedPnL[pos.token] || 0,
|
|
606
|
+
holdingDuration: now - pos.entryTimestamp
|
|
607
|
+
})).sort((a, b) => Math.abs(b.unrealizedPnL) - Math.abs(a.unrealizedPnL)).slice(0, 5);
|
|
608
|
+
const oneDayAgo = now - 24 * 60 * 60 * 1e3;
|
|
609
|
+
const recentTrades = this.tradeHistory.filter((t) => t.timestamp > oneDayAgo).length;
|
|
610
|
+
const totalRealizedPnL = this.tradeHistory.filter((t) => t.realizedPnL !== void 0).reduce((sum, t) => sum + (t.realizedPnL || 0), 0);
|
|
611
|
+
return {
|
|
612
|
+
openPositions: Object.keys(this.positions).length,
|
|
613
|
+
totalUnrealizedPnL: Object.values(unrealizedPnL).reduce((s, v) => s + v, 0),
|
|
614
|
+
topPositions,
|
|
615
|
+
recentTrades,
|
|
616
|
+
totalRealizedPnL
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
// ============================================================
|
|
620
|
+
// INTERNAL
|
|
621
|
+
// ============================================================
|
|
622
|
+
persist() {
|
|
623
|
+
this.store.set(KEY_POSITIONS, this.positions);
|
|
624
|
+
this.store.set(KEY_TRADE_HISTORY, this.tradeHistory);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/llm/openai.ts
|
|
629
|
+
var import_openai = __toESM(require("openai"));
|
|
630
|
+
|
|
631
|
+
// src/llm/base.ts
|
|
632
|
+
var BaseLLMAdapter = class {
|
|
633
|
+
config;
|
|
634
|
+
constructor(config) {
|
|
635
|
+
this.config = config;
|
|
636
|
+
}
|
|
637
|
+
getMetadata() {
|
|
638
|
+
return {
|
|
639
|
+
provider: this.config.provider,
|
|
640
|
+
model: this.config.model || "unknown",
|
|
641
|
+
isLocal: this.config.provider === "ollama"
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Format model name for display
|
|
646
|
+
*/
|
|
647
|
+
getDisplayModel() {
|
|
648
|
+
if (this.config.provider === "ollama") {
|
|
649
|
+
return `Local (${this.config.model || "ollama"})`;
|
|
650
|
+
}
|
|
651
|
+
return this.config.model || this.config.provider;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// src/llm/openai.ts
|
|
656
|
+
var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
657
|
+
client;
|
|
658
|
+
constructor(config) {
|
|
659
|
+
super(config);
|
|
660
|
+
if (!config.apiKey && !config.endpoint) {
|
|
661
|
+
throw new Error("OpenAI API key or custom endpoint required");
|
|
662
|
+
}
|
|
663
|
+
this.client = new import_openai.default({
|
|
664
|
+
apiKey: config.apiKey || "not-needed-for-custom",
|
|
665
|
+
baseURL: config.endpoint
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async chat(messages) {
|
|
669
|
+
try {
|
|
670
|
+
const response = await this.client.chat.completions.create({
|
|
671
|
+
model: this.config.model || "gpt-4.1",
|
|
672
|
+
messages: messages.map((m) => ({
|
|
673
|
+
role: m.role,
|
|
674
|
+
content: m.content
|
|
675
|
+
})),
|
|
676
|
+
temperature: this.config.temperature,
|
|
677
|
+
max_tokens: this.config.maxTokens
|
|
678
|
+
});
|
|
679
|
+
const choice = response.choices[0];
|
|
680
|
+
if (!choice || !choice.message) {
|
|
681
|
+
throw new Error("No response from OpenAI");
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
content: choice.message.content || "",
|
|
685
|
+
usage: response.usage ? {
|
|
686
|
+
promptTokens: response.usage.prompt_tokens,
|
|
687
|
+
completionTokens: response.usage.completion_tokens,
|
|
688
|
+
totalTokens: response.usage.total_tokens
|
|
689
|
+
} : void 0
|
|
690
|
+
};
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (error instanceof import_openai.default.APIError) {
|
|
693
|
+
throw new Error(`OpenAI API error: ${error.message}`);
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/llm/anthropic.ts
|
|
701
|
+
var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
702
|
+
apiKey;
|
|
703
|
+
baseUrl;
|
|
704
|
+
constructor(config) {
|
|
705
|
+
super(config);
|
|
706
|
+
if (!config.apiKey) {
|
|
707
|
+
throw new Error("Anthropic API key required");
|
|
708
|
+
}
|
|
709
|
+
this.apiKey = config.apiKey;
|
|
710
|
+
this.baseUrl = config.endpoint || "https://api.anthropic.com";
|
|
711
|
+
}
|
|
712
|
+
async chat(messages) {
|
|
713
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
714
|
+
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
715
|
+
const body = {
|
|
716
|
+
model: this.config.model || "claude-opus-4-5-20251101",
|
|
717
|
+
max_tokens: this.config.maxTokens || 4096,
|
|
718
|
+
temperature: this.config.temperature,
|
|
719
|
+
system: systemMessage?.content,
|
|
720
|
+
messages: chatMessages.map((m) => ({
|
|
721
|
+
role: m.role,
|
|
184
722
|
content: m.content
|
|
185
723
|
}))
|
|
186
724
|
};
|
|
@@ -1073,441 +1611,168 @@ function createSampleConfig(agentId, name) {
|
|
|
1073
1611
|
llm: {
|
|
1074
1612
|
provider: "openai",
|
|
1075
1613
|
model: "gpt-4.1",
|
|
1076
|
-
temperature: 0.7,
|
|
1077
|
-
maxTokens: 4096
|
|
1078
|
-
},
|
|
1079
|
-
riskUniverse: "established",
|
|
1080
|
-
trading: {
|
|
1081
|
-
timeHorizon: "swing",
|
|
1082
|
-
maxPositionSizeBps: 1e3,
|
|
1083
|
-
maxDailyLossBps: 500,
|
|
1084
|
-
maxConcurrentPositions: 5,
|
|
1085
|
-
tradingIntervalMs: 6e4,
|
|
1086
|
-
maxSlippageBps: 100,
|
|
1087
|
-
minTradeValueUSD: 1
|
|
1088
|
-
},
|
|
1089
|
-
vault: {
|
|
1090
|
-
// Default to manual - user must explicitly enable auto-creation
|
|
1091
|
-
policy: "manual",
|
|
1092
|
-
// Will use agent name for vault name if not set
|
|
1093
|
-
preferVaultTrading: true
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// src/trading/executor.ts
|
|
1099
|
-
var TradeExecutor = class {
|
|
1100
|
-
client;
|
|
1101
|
-
config;
|
|
1102
|
-
allowedTokens;
|
|
1103
|
-
configHashFn;
|
|
1104
|
-
constructor(client, config, configHashFn) {
|
|
1105
|
-
this.client = client;
|
|
1106
|
-
this.config = config;
|
|
1107
|
-
this.configHashFn = configHashFn;
|
|
1108
|
-
this.allowedTokens = new Set(
|
|
1109
|
-
(config.allowedTokens || []).map((t) => t.toLowerCase())
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Execute a single trade signal
|
|
1114
|
-
*/
|
|
1115
|
-
async execute(signal) {
|
|
1116
|
-
if (signal.action === "hold") {
|
|
1117
|
-
return { success: true };
|
|
1118
|
-
}
|
|
1119
|
-
try {
|
|
1120
|
-
console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
|
|
1121
|
-
console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
|
|
1122
|
-
if (!this.validateSignal(signal)) {
|
|
1123
|
-
return { success: false, error: "Signal exceeds position limits" };
|
|
1124
|
-
}
|
|
1125
|
-
const configHash = this.configHashFn?.();
|
|
1126
|
-
const result = await this.client.trade({
|
|
1127
|
-
tokenIn: signal.tokenIn,
|
|
1128
|
-
tokenOut: signal.tokenOut,
|
|
1129
|
-
amountIn: signal.amountIn,
|
|
1130
|
-
maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
|
|
1131
|
-
...configHash && { configHash }
|
|
1132
|
-
});
|
|
1133
|
-
console.log(`Trade executed: ${result.hash}`);
|
|
1134
|
-
return { success: true, txHash: result.hash };
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1137
|
-
const classified = classifyTradeError(message);
|
|
1138
|
-
console.error(`Trade failed (${classified.category}): ${classified.userMessage}`);
|
|
1139
|
-
return { success: false, error: classified.userMessage };
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Execute multiple trade signals
|
|
1144
|
-
* Returns results for each signal
|
|
1145
|
-
*/
|
|
1146
|
-
async executeAll(signals) {
|
|
1147
|
-
const results = [];
|
|
1148
|
-
for (const signal of signals) {
|
|
1149
|
-
const result = await this.execute(signal);
|
|
1150
|
-
results.push({ signal, ...result });
|
|
1151
|
-
if (signals.indexOf(signal) < signals.length - 1) {
|
|
1152
|
-
await this.delay(1e3);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
return results;
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Validate a signal against config limits and token restrictions
|
|
1159
|
-
*/
|
|
1160
|
-
validateSignal(signal) {
|
|
1161
|
-
if (signal.confidence < 0.5) {
|
|
1162
|
-
console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
|
|
1163
|
-
return false;
|
|
1164
|
-
}
|
|
1165
|
-
if (this.allowedTokens.size > 0) {
|
|
1166
|
-
const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
|
|
1167
|
-
const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
|
|
1168
|
-
if (!tokenInAllowed) {
|
|
1169
|
-
console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
|
|
1170
|
-
return false;
|
|
1171
|
-
}
|
|
1172
|
-
if (!tokenOutAllowed) {
|
|
1173
|
-
console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
|
|
1174
|
-
return false;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
return true;
|
|
1178
|
-
}
|
|
1179
|
-
delay(ms) {
|
|
1180
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1181
|
-
}
|
|
1182
|
-
};
|
|
1183
|
-
function classifyTradeError(message) {
|
|
1184
|
-
const lower = message.toLowerCase();
|
|
1185
|
-
if (lower.includes("configmismatch") || lower.includes("config mismatch")) {
|
|
1186
|
-
return {
|
|
1187
|
-
category: "config_mismatch",
|
|
1188
|
-
userMessage: "Config hash mismatch \u2014 on-chain config does not match. Restart the agent to re-sync."
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
if (lower.includes("insufficient funds") || lower.includes("exceeds the balance")) {
|
|
1192
|
-
return {
|
|
1193
|
-
category: "insufficient_funds",
|
|
1194
|
-
userMessage: "Insufficient ETH for gas \u2014 fund your trading wallet."
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
if (lower.includes("out of gas") || lower.includes("intrinsic gas too low")) {
|
|
1198
|
-
return {
|
|
1199
|
-
category: "out_of_gas",
|
|
1200
|
-
userMessage: "Transaction ran out of gas during execution. This is usually transient \u2014 retrying with more gas."
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
if (lower.includes("minamountnotmet") || lower.includes("min amount") || lower.includes("slippage")) {
|
|
1204
|
-
return {
|
|
1205
|
-
category: "slippage",
|
|
1206
|
-
userMessage: "Swap failed due to slippage \u2014 price moved beyond tolerance. Will retry next cycle."
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
if (lower.includes("notauthorizedforagent") || lower.includes("not authorized")) {
|
|
1210
|
-
return {
|
|
1211
|
-
category: "not_authorized",
|
|
1212
|
-
userMessage: "Wallet not authorized for this agent. Ensure your wallet is linked via the registry."
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
if (lower.includes("aggregatornotwhitelisted")) {
|
|
1216
|
-
return {
|
|
1217
|
-
category: "aggregator_error",
|
|
1218
|
-
userMessage: "DEX aggregator not whitelisted on the router. Contact support."
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
if (lower.includes("reverted") || lower.includes("execution reverted")) {
|
|
1222
|
-
return {
|
|
1223
|
-
category: "reverted",
|
|
1224
|
-
userMessage: `Transaction reverted: ${message.slice(0, 200)}`
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
|
|
1228
|
-
return {
|
|
1229
|
-
category: "network",
|
|
1230
|
-
userMessage: "Network error \u2014 RPC node may be down. Will retry next cycle."
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
return {
|
|
1234
|
-
category: "unknown",
|
|
1235
|
-
userMessage: message.slice(0, 300)
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// src/trading/market.ts
|
|
1240
|
-
var import_viem = require("viem");
|
|
1241
|
-
var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
1242
|
-
var TOKEN_DECIMALS = {
|
|
1243
|
-
// Base Mainnet — Core tokens
|
|
1244
|
-
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
1245
|
-
// USDC
|
|
1246
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
1247
|
-
// USDbC
|
|
1248
|
-
"0x4200000000000000000000000000000000000006": 18,
|
|
1249
|
-
// WETH
|
|
1250
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
1251
|
-
// DAI
|
|
1252
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
1253
|
-
// cbETH
|
|
1254
|
-
[NATIVE_ETH.toLowerCase()]: 18,
|
|
1255
|
-
// Native ETH
|
|
1256
|
-
// Base Mainnet — Established tokens
|
|
1257
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
1258
|
-
// AERO (Aerodrome)
|
|
1259
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
1260
|
-
// BRETT
|
|
1261
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
1262
|
-
// DEGEN
|
|
1263
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
1264
|
-
// VIRTUAL
|
|
1265
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
1266
|
-
// TOSHI
|
|
1267
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
1268
|
-
// cbBTC
|
|
1269
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
1270
|
-
// ezETH (Renzo)
|
|
1271
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
1272
|
-
// wstETH (Lido)
|
|
1273
|
-
};
|
|
1274
|
-
function getTokenDecimals(address) {
|
|
1275
|
-
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
1276
|
-
if (decimals === void 0) {
|
|
1277
|
-
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
1278
|
-
return 18;
|
|
1279
|
-
}
|
|
1280
|
-
return decimals;
|
|
1281
|
-
}
|
|
1282
|
-
var TOKEN_TO_COINGECKO = {
|
|
1283
|
-
// Core
|
|
1284
|
-
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
1285
|
-
// WETH
|
|
1286
|
-
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
1287
|
-
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
1288
|
-
// USDC
|
|
1289
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
1290
|
-
// USDbC
|
|
1291
|
-
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
1292
|
-
// cbETH
|
|
1293
|
-
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
1294
|
-
// DAI
|
|
1295
|
-
// Established
|
|
1296
|
-
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
1297
|
-
// AERO
|
|
1298
|
-
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
1299
|
-
// BRETT
|
|
1300
|
-
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
1301
|
-
// DEGEN
|
|
1302
|
-
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
1303
|
-
// VIRTUAL
|
|
1304
|
-
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
1305
|
-
// TOSHI
|
|
1306
|
-
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
1307
|
-
// cbBTC
|
|
1308
|
-
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
1309
|
-
// ezETH
|
|
1310
|
-
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
1311
|
-
// wstETH
|
|
1312
|
-
};
|
|
1313
|
-
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
1314
|
-
var PRICE_STALENESS_MS = 6e4;
|
|
1315
|
-
var MarketDataService = class {
|
|
1316
|
-
rpcUrl;
|
|
1317
|
-
client;
|
|
1318
|
-
/** Cached prices from last fetch */
|
|
1319
|
-
cachedPrices = {};
|
|
1320
|
-
/** Timestamp of last successful price fetch */
|
|
1321
|
-
lastPriceFetchAt = 0;
|
|
1322
|
-
constructor(rpcUrl) {
|
|
1323
|
-
this.rpcUrl = rpcUrl;
|
|
1324
|
-
this.client = (0, import_viem.createPublicClient)({
|
|
1325
|
-
transport: (0, import_viem.http)(rpcUrl)
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
/** Cached volume data */
|
|
1329
|
-
cachedVolume24h = {};
|
|
1330
|
-
/** Cached price change data */
|
|
1331
|
-
cachedPriceChange24h = {};
|
|
1332
|
-
/**
|
|
1333
|
-
* Fetch current market data for the agent
|
|
1334
|
-
*/
|
|
1335
|
-
async fetchMarketData(walletAddress, tokenAddresses) {
|
|
1336
|
-
const prices = await this.fetchPrices(tokenAddresses);
|
|
1337
|
-
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
1338
|
-
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
1339
|
-
let gasPrice;
|
|
1340
|
-
try {
|
|
1341
|
-
gasPrice = await this.client.getGasPrice();
|
|
1342
|
-
} catch {
|
|
1343
|
-
}
|
|
1344
|
-
return {
|
|
1345
|
-
timestamp: Date.now(),
|
|
1346
|
-
prices,
|
|
1347
|
-
balances,
|
|
1348
|
-
portfolioValue,
|
|
1349
|
-
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1350
|
-
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1351
|
-
gasPrice,
|
|
1352
|
-
network: {
|
|
1353
|
-
chainId: this.client.chain?.id ?? 8453
|
|
1354
|
-
}
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* Check if cached prices are still fresh
|
|
1359
|
-
*/
|
|
1360
|
-
get pricesAreFresh() {
|
|
1361
|
-
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
1362
|
-
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Fetch token prices from CoinGecko free API
|
|
1365
|
-
* Returns cached prices if still fresh (<60s old)
|
|
1366
|
-
*/
|
|
1367
|
-
async fetchPrices(tokenAddresses) {
|
|
1368
|
-
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
1369
|
-
const prices2 = { ...this.cachedPrices };
|
|
1370
|
-
for (const addr of tokenAddresses) {
|
|
1371
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1372
|
-
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1373
|
-
prices2[addr.toLowerCase()] = 1;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
return prices2;
|
|
1377
|
-
}
|
|
1378
|
-
const prices = {};
|
|
1379
|
-
const idsToFetch = /* @__PURE__ */ new Set();
|
|
1380
|
-
for (const addr of tokenAddresses) {
|
|
1381
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1382
|
-
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1383
|
-
idsToFetch.add(cgId);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
idsToFetch.add("ethereum");
|
|
1387
|
-
if (idsToFetch.size > 0) {
|
|
1388
|
-
try {
|
|
1389
|
-
const ids = Array.from(idsToFetch).join(",");
|
|
1390
|
-
const response = await fetch(
|
|
1391
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1392
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1393
|
-
);
|
|
1394
|
-
if (response.ok) {
|
|
1395
|
-
const data = await response.json();
|
|
1396
|
-
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1397
|
-
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1398
|
-
if (id === cgId) {
|
|
1399
|
-
const key = addr.toLowerCase();
|
|
1400
|
-
prices[key] = priceData.usd;
|
|
1401
|
-
if (priceData.usd_24h_vol !== void 0) {
|
|
1402
|
-
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1403
|
-
}
|
|
1404
|
-
if (priceData.usd_24h_change !== void 0) {
|
|
1405
|
-
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
this.lastPriceFetchAt = Date.now();
|
|
1411
|
-
} else {
|
|
1412
|
-
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1413
|
-
}
|
|
1414
|
-
} catch (error) {
|
|
1415
|
-
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
for (const addr of tokenAddresses) {
|
|
1419
|
-
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1420
|
-
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
1421
|
-
prices[addr.toLowerCase()] = 1;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
const missingAddrs = tokenAddresses.filter(
|
|
1425
|
-
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
1426
|
-
);
|
|
1427
|
-
if (missingAddrs.length > 0) {
|
|
1428
|
-
try {
|
|
1429
|
-
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1430
|
-
const llamaResponse = await fetch(
|
|
1431
|
-
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1432
|
-
{ signal: AbortSignal.timeout(5e3) }
|
|
1433
|
-
);
|
|
1434
|
-
if (llamaResponse.ok) {
|
|
1435
|
-
const llamaData = await llamaResponse.json();
|
|
1436
|
-
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1437
|
-
const addr = key.replace("base:", "").toLowerCase();
|
|
1438
|
-
if (data.price && data.confidence > 0.5) {
|
|
1439
|
-
prices[addr] = data.price;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1443
|
-
}
|
|
1444
|
-
} catch {
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
if (Object.keys(prices).length > 0) {
|
|
1448
|
-
this.cachedPrices = prices;
|
|
1449
|
-
}
|
|
1450
|
-
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1451
|
-
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1452
|
-
return { ...this.cachedPrices };
|
|
1453
|
-
}
|
|
1454
|
-
for (const addr of tokenAddresses) {
|
|
1455
|
-
if (!prices[addr.toLowerCase()]) {
|
|
1456
|
-
console.warn(`No price available for ${addr}, using 0`);
|
|
1457
|
-
prices[addr.toLowerCase()] = 0;
|
|
1458
|
-
}
|
|
1614
|
+
temperature: 0.7,
|
|
1615
|
+
maxTokens: 4096
|
|
1616
|
+
},
|
|
1617
|
+
riskUniverse: "established",
|
|
1618
|
+
trading: {
|
|
1619
|
+
timeHorizon: "swing",
|
|
1620
|
+
maxPositionSizeBps: 1e3,
|
|
1621
|
+
maxDailyLossBps: 500,
|
|
1622
|
+
maxConcurrentPositions: 5,
|
|
1623
|
+
tradingIntervalMs: 6e4,
|
|
1624
|
+
maxSlippageBps: 100,
|
|
1625
|
+
minTradeValueUSD: 1
|
|
1626
|
+
},
|
|
1627
|
+
vault: {
|
|
1628
|
+
// Default to manual - user must explicitly enable auto-creation
|
|
1629
|
+
policy: "manual",
|
|
1630
|
+
// Will use agent name for vault name if not set
|
|
1631
|
+
preferVaultTrading: true
|
|
1459
1632
|
}
|
|
1460
|
-
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// src/trading/executor.ts
|
|
1637
|
+
var TradeExecutor = class {
|
|
1638
|
+
client;
|
|
1639
|
+
config;
|
|
1640
|
+
allowedTokens;
|
|
1641
|
+
configHashFn;
|
|
1642
|
+
constructor(client, config, configHashFn) {
|
|
1643
|
+
this.client = client;
|
|
1644
|
+
this.config = config;
|
|
1645
|
+
this.configHashFn = configHashFn;
|
|
1646
|
+
this.allowedTokens = new Set(
|
|
1647
|
+
(config.allowedTokens || []).map((t) => t.toLowerCase())
|
|
1648
|
+
);
|
|
1461
1649
|
}
|
|
1462
1650
|
/**
|
|
1463
|
-
*
|
|
1651
|
+
* Execute a single trade signal
|
|
1464
1652
|
*/
|
|
1465
|
-
async
|
|
1466
|
-
|
|
1467
|
-
|
|
1653
|
+
async execute(signal) {
|
|
1654
|
+
if (signal.action === "hold") {
|
|
1655
|
+
return { success: true };
|
|
1656
|
+
}
|
|
1468
1657
|
try {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const balance = await this.client.readContract({
|
|
1474
|
-
address: tokenAddress,
|
|
1475
|
-
abi: import_viem.erc20Abi,
|
|
1476
|
-
functionName: "balanceOf",
|
|
1477
|
-
args: [wallet]
|
|
1478
|
-
});
|
|
1479
|
-
return { address: tokenAddress.toLowerCase(), balance };
|
|
1480
|
-
} catch (error) {
|
|
1481
|
-
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
1482
|
-
}
|
|
1483
|
-
});
|
|
1484
|
-
const results = await Promise.all(erc20Promises);
|
|
1485
|
-
for (const { address, balance } of results) {
|
|
1486
|
-
balances[address] = balance;
|
|
1658
|
+
console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
|
|
1659
|
+
console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
|
|
1660
|
+
if (!this.validateSignal(signal)) {
|
|
1661
|
+
return { success: false, error: "Signal exceeds position limits" };
|
|
1487
1662
|
}
|
|
1663
|
+
const configHash = this.configHashFn?.();
|
|
1664
|
+
const result = await this.client.trade({
|
|
1665
|
+
tokenIn: signal.tokenIn,
|
|
1666
|
+
tokenOut: signal.tokenOut,
|
|
1667
|
+
amountIn: signal.amountIn,
|
|
1668
|
+
maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
|
|
1669
|
+
...configHash && { configHash }
|
|
1670
|
+
});
|
|
1671
|
+
console.log(`Trade executed: ${result.hash}`);
|
|
1672
|
+
return { success: true, txHash: result.hash };
|
|
1488
1673
|
} catch (error) {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1674
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1675
|
+
const classified = classifyTradeError(message);
|
|
1676
|
+
console.error(`Trade failed (${classified.category}): ${classified.userMessage}`);
|
|
1677
|
+
return { success: false, error: classified.userMessage };
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Execute multiple trade signals
|
|
1682
|
+
* Returns results for each signal
|
|
1683
|
+
*/
|
|
1684
|
+
async executeAll(signals) {
|
|
1685
|
+
const results = [];
|
|
1686
|
+
for (const signal of signals) {
|
|
1687
|
+
const result = await this.execute(signal);
|
|
1688
|
+
results.push({ signal, ...result });
|
|
1689
|
+
if (signals.indexOf(signal) < signals.length - 1) {
|
|
1690
|
+
await this.delay(1e3);
|
|
1493
1691
|
}
|
|
1494
1692
|
}
|
|
1495
|
-
return
|
|
1693
|
+
return results;
|
|
1496
1694
|
}
|
|
1497
1695
|
/**
|
|
1498
|
-
*
|
|
1696
|
+
* Validate a signal against config limits and token restrictions
|
|
1499
1697
|
*/
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
const decimals = getTokenDecimals(address);
|
|
1505
|
-
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1506
|
-
total += amount * price;
|
|
1698
|
+
validateSignal(signal) {
|
|
1699
|
+
if (signal.confidence < 0.5) {
|
|
1700
|
+
console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
|
|
1701
|
+
return false;
|
|
1507
1702
|
}
|
|
1508
|
-
|
|
1703
|
+
if (this.allowedTokens.size > 0) {
|
|
1704
|
+
const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
|
|
1705
|
+
const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
|
|
1706
|
+
if (!tokenInAllowed) {
|
|
1707
|
+
console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
if (!tokenOutAllowed) {
|
|
1711
|
+
console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
delay(ms) {
|
|
1718
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1509
1719
|
}
|
|
1510
1720
|
};
|
|
1721
|
+
function classifyTradeError(message) {
|
|
1722
|
+
const lower = message.toLowerCase();
|
|
1723
|
+
if (lower.includes("configmismatch") || lower.includes("config mismatch")) {
|
|
1724
|
+
return {
|
|
1725
|
+
category: "config_mismatch",
|
|
1726
|
+
userMessage: "Config hash mismatch \u2014 on-chain config does not match. Restart the agent to re-sync."
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
if (lower.includes("insufficient funds") || lower.includes("exceeds the balance")) {
|
|
1730
|
+
return {
|
|
1731
|
+
category: "insufficient_funds",
|
|
1732
|
+
userMessage: "Insufficient ETH for gas \u2014 fund your trading wallet."
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
if (lower.includes("out of gas") || lower.includes("intrinsic gas too low")) {
|
|
1736
|
+
return {
|
|
1737
|
+
category: "out_of_gas",
|
|
1738
|
+
userMessage: "Transaction ran out of gas during execution. This is usually transient \u2014 retrying with more gas."
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
if (lower.includes("minamountnotmet") || lower.includes("min amount") || lower.includes("slippage")) {
|
|
1742
|
+
return {
|
|
1743
|
+
category: "slippage",
|
|
1744
|
+
userMessage: "Swap failed due to slippage \u2014 price moved beyond tolerance. Will retry next cycle."
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
if (lower.includes("notauthorizedforagent") || lower.includes("not authorized")) {
|
|
1748
|
+
return {
|
|
1749
|
+
category: "not_authorized",
|
|
1750
|
+
userMessage: "Wallet not authorized for this agent. Ensure your wallet is linked via the registry."
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
if (lower.includes("aggregatornotwhitelisted")) {
|
|
1754
|
+
return {
|
|
1755
|
+
category: "aggregator_error",
|
|
1756
|
+
userMessage: "DEX aggregator not whitelisted on the router. Contact support."
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
if (lower.includes("reverted") || lower.includes("execution reverted")) {
|
|
1760
|
+
return {
|
|
1761
|
+
category: "reverted",
|
|
1762
|
+
userMessage: `Transaction reverted: ${message.slice(0, 200)}`
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
|
|
1766
|
+
return {
|
|
1767
|
+
category: "network",
|
|
1768
|
+
userMessage: "Network error \u2014 RPC node may be down. Will retry next cycle."
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
category: "unknown",
|
|
1773
|
+
userMessage: message.slice(0, 300)
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1511
1776
|
|
|
1512
1777
|
// src/trading/risk.ts
|
|
1513
1778
|
var RiskManager = class {
|
|
@@ -1619,6 +1884,31 @@ var RiskManager = class {
|
|
|
1619
1884
|
updateFees(fees) {
|
|
1620
1885
|
this.dailyFees += fees;
|
|
1621
1886
|
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Export current risk state for persistence across restarts.
|
|
1889
|
+
*/
|
|
1890
|
+
exportState() {
|
|
1891
|
+
return {
|
|
1892
|
+
dailyPnL: this.dailyPnL,
|
|
1893
|
+
dailyFees: this.dailyFees,
|
|
1894
|
+
lastResetDate: this.lastResetDate
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Restore risk state from persistence (called on startup).
|
|
1899
|
+
* Only restores if the saved state is from today — expired state is ignored.
|
|
1900
|
+
*/
|
|
1901
|
+
restoreState(state) {
|
|
1902
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1903
|
+
if (state.lastResetDate === today) {
|
|
1904
|
+
this.dailyPnL = state.dailyPnL;
|
|
1905
|
+
this.dailyFees = state.dailyFees;
|
|
1906
|
+
this.lastResetDate = state.lastResetDate;
|
|
1907
|
+
console.log(`Risk state restored: PnL=$${this.dailyPnL.toFixed(2)}, Fees=$${this.dailyFees.toFixed(2)}`);
|
|
1908
|
+
} else {
|
|
1909
|
+
console.log("Risk state expired (different day), starting fresh");
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1622
1912
|
/**
|
|
1623
1913
|
* Get current risk status
|
|
1624
1914
|
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
@@ -2082,7 +2372,6 @@ var VaultManager = class {
|
|
|
2082
2372
|
// src/relay.ts
|
|
2083
2373
|
var import_ws = __toESM(require("ws"));
|
|
2084
2374
|
var import_accounts2 = require("viem/accounts");
|
|
2085
|
-
var import_sdk = require("@exagent/sdk");
|
|
2086
2375
|
var RelayClient = class {
|
|
2087
2376
|
config;
|
|
2088
2377
|
ws = null;
|
|
@@ -2186,7 +2475,7 @@ var RelayClient = class {
|
|
|
2186
2475
|
wallet: account.address,
|
|
2187
2476
|
timestamp,
|
|
2188
2477
|
signature,
|
|
2189
|
-
sdkVersion:
|
|
2478
|
+
sdkVersion: AGENT_VERSION
|
|
2190
2479
|
});
|
|
2191
2480
|
}
|
|
2192
2481
|
/**
|
|
@@ -3597,14 +3886,19 @@ var AgentRuntime = class {
|
|
|
3597
3886
|
isRunning = false;
|
|
3598
3887
|
mode = "idle";
|
|
3599
3888
|
configHash;
|
|
3889
|
+
pendingConfigHash = null;
|
|
3890
|
+
lastConfigCheckAt = 0;
|
|
3891
|
+
// Timestamp of last pending config RPC check
|
|
3600
3892
|
cycleCount = 0;
|
|
3601
3893
|
lastCycleAt = 0;
|
|
3602
3894
|
lastPortfolioValue = 0;
|
|
3603
3895
|
lastEthBalance = "0";
|
|
3896
|
+
lastPrices = {};
|
|
3604
3897
|
processAlive = true;
|
|
3605
3898
|
riskUniverse = 0;
|
|
3606
3899
|
allowedTokens = /* @__PURE__ */ new Set();
|
|
3607
3900
|
strategyContext;
|
|
3901
|
+
positionTracker;
|
|
3608
3902
|
// Perp trading components (null if perp not enabled)
|
|
3609
3903
|
perpClient = null;
|
|
3610
3904
|
perpSigner = null;
|
|
@@ -3621,6 +3915,12 @@ var AgentRuntime = class {
|
|
|
3621
3915
|
// When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
|
|
3622
3916
|
perpConnected = false;
|
|
3623
3917
|
perpTradingActive = false;
|
|
3918
|
+
// Cached perp account data for synchronous heartbeat inclusion (refreshed async)
|
|
3919
|
+
cachedPerpEquity = 0;
|
|
3920
|
+
cachedPerpUnrealizedPnl = 0;
|
|
3921
|
+
cachedPerpMarginUsed = 0;
|
|
3922
|
+
cachedPerpLeverage = 0;
|
|
3923
|
+
cachedPerpOpenPositions = 0;
|
|
3624
3924
|
constructor(config) {
|
|
3625
3925
|
this.config = config;
|
|
3626
3926
|
}
|
|
@@ -3629,7 +3929,7 @@ var AgentRuntime = class {
|
|
|
3629
3929
|
*/
|
|
3630
3930
|
async initialize() {
|
|
3631
3931
|
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
3632
|
-
this.client = new
|
|
3932
|
+
this.client = new import_sdk.ExagentClient({
|
|
3633
3933
|
privateKey: this.config.privateKey,
|
|
3634
3934
|
network: this.config.network
|
|
3635
3935
|
});
|
|
@@ -3647,14 +3947,20 @@ var AgentRuntime = class {
|
|
|
3647
3947
|
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
3648
3948
|
await this.syncConfigHash();
|
|
3649
3949
|
this.strategy = await loadStrategy();
|
|
3950
|
+
const store = new FileStore();
|
|
3650
3951
|
this.strategyContext = {
|
|
3651
|
-
store
|
|
3952
|
+
store,
|
|
3652
3953
|
agentId: Number(this.config.agentId),
|
|
3653
3954
|
walletAddress: this.client.address
|
|
3654
3955
|
};
|
|
3956
|
+
this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
|
|
3655
3957
|
this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
|
|
3656
3958
|
this.riskManager = new RiskManager(this.config.trading);
|
|
3657
3959
|
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
3960
|
+
const savedRisk = this.positionTracker.getRiskState();
|
|
3961
|
+
if (savedRisk.lastResetDate) {
|
|
3962
|
+
this.riskManager.restoreState(savedRisk);
|
|
3963
|
+
}
|
|
3658
3964
|
await this.initializeVaultManager();
|
|
3659
3965
|
await this.initializePerp();
|
|
3660
3966
|
await this.initializeRelay();
|
|
@@ -3840,7 +4146,7 @@ var AgentRuntime = class {
|
|
|
3840
4146
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
3841
4147
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
3842
4148
|
const nonce = await this.client.registry.getNonce(address);
|
|
3843
|
-
const linkMessage =
|
|
4149
|
+
const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
|
|
3844
4150
|
address,
|
|
3845
4151
|
agentId,
|
|
3846
4152
|
nonce
|
|
@@ -3925,65 +4231,104 @@ var AgentRuntime = class {
|
|
|
3925
4231
|
}
|
|
3926
4232
|
/**
|
|
3927
4233
|
* Sync the LLM config hash to chain for epoch tracking.
|
|
3928
|
-
*
|
|
3929
|
-
*
|
|
4234
|
+
*
|
|
4235
|
+
* If the trading wallet is NOT the agent owner, the on-chain setConfig
|
|
4236
|
+
* call would revert with AgentNotOwner. Instead, we set a pending state
|
|
4237
|
+
* and send a message to the command center so the owner can approve it
|
|
4238
|
+
* from the website with one click.
|
|
4239
|
+
*
|
|
4240
|
+
* Until the config is verified on-chain, the agent won't appear on the
|
|
4241
|
+
* leaderboard (trades still execute normally).
|
|
3930
4242
|
*/
|
|
3931
4243
|
async syncConfigHash() {
|
|
3932
4244
|
const agentId = BigInt(this.config.agentId);
|
|
3933
4245
|
const llmMeta = this.llm.getMetadata();
|
|
3934
|
-
this.configHash =
|
|
4246
|
+
this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
3935
4247
|
console.log(`Config hash: ${this.configHash}`);
|
|
3936
4248
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
3937
|
-
if (onChainHash
|
|
3938
|
-
console.log("Config
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4249
|
+
if (onChainHash === this.configHash) {
|
|
4250
|
+
console.log("Config hash matches on-chain");
|
|
4251
|
+
this.pendingConfigHash = null;
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
console.log("Config changed, updating on-chain...");
|
|
4255
|
+
const agent = await this.client.registry.getAgent(agentId);
|
|
4256
|
+
const isOwner = agent?.owner.toLowerCase() === this.client.address.toLowerCase();
|
|
4257
|
+
if (!isOwner) {
|
|
4258
|
+
this.pendingConfigHash = this.configHash;
|
|
4259
|
+
this.configHash = onChainHash;
|
|
4260
|
+
console.log("");
|
|
4261
|
+
console.log("=== CONFIG VERIFICATION NEEDED ===");
|
|
4262
|
+
console.log("");
|
|
4263
|
+
console.log(" Your trading wallet cannot update the LLM config on-chain.");
|
|
4264
|
+
console.log(" The owner must approve this from the command center.");
|
|
4265
|
+
console.log("");
|
|
4266
|
+
console.log(` LLM: ${llmMeta.provider} / ${llmMeta.model}`);
|
|
4267
|
+
console.log(` Hash: ${this.pendingConfigHash}`);
|
|
4268
|
+
console.log("");
|
|
4269
|
+
console.log(" Until approved:");
|
|
4270
|
+
console.log(" - New agents will not appear on the leaderboard");
|
|
4271
|
+
console.log(" - Trades will still execute normally");
|
|
4272
|
+
console.log("");
|
|
4273
|
+
this.relay?.sendMessage(
|
|
4274
|
+
"system",
|
|
4275
|
+
"warning",
|
|
4276
|
+
"Config Verification Needed",
|
|
4277
|
+
`Your agent is using ${llmMeta.provider}/${llmMeta.model} but this hasn't been recorded on-chain. Open the command center and click "Approve Config" to verify. Until then, your agent won't appear on the leaderboard.`,
|
|
4278
|
+
{ configHash: this.pendingConfigHash, provider: llmMeta.provider, model: llmMeta.model }
|
|
4279
|
+
);
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
try {
|
|
4283
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
4284
|
+
console.log(`Config updated on-chain`);
|
|
4285
|
+
this.pendingConfigHash = null;
|
|
4286
|
+
} catch (error) {
|
|
4287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4288
|
+
if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
4289
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
4290
|
+
const publicClientInstance = (0, import_viem6.createPublicClient)({
|
|
4291
|
+
chain: import_chains4.base,
|
|
4292
|
+
transport: (0, import_viem6.http)(this.getRpcUrl())
|
|
4293
|
+
});
|
|
4294
|
+
console.log("");
|
|
4295
|
+
console.log("=== ETH NEEDED FOR GAS ===");
|
|
4296
|
+
console.log("");
|
|
4297
|
+
console.log(` Wallet: ${this.client.address}`);
|
|
4298
|
+
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
4299
|
+
console.log(" Opening the command center to fund your wallet...");
|
|
4300
|
+
console.log(` ${ccUrl}`);
|
|
4301
|
+
console.log("");
|
|
4302
|
+
openBrowser(ccUrl);
|
|
4303
|
+
console.log(" Waiting for ETH... (checking every 15s)");
|
|
4304
|
+
console.log(" Press Ctrl+C to exit.");
|
|
4305
|
+
console.log("");
|
|
4306
|
+
while (true) {
|
|
4307
|
+
await this.sleep(15e3);
|
|
4308
|
+
const balance = await publicClientInstance.getBalance({
|
|
4309
|
+
address: this.client.address
|
|
3951
4310
|
});
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
3957
|
-
console.log(" Opening the command center to fund your wallet...");
|
|
3958
|
-
console.log(` ${ccUrl}`);
|
|
3959
|
-
console.log("");
|
|
3960
|
-
openBrowser(ccUrl);
|
|
3961
|
-
console.log(" Waiting for ETH... (checking every 15s)");
|
|
3962
|
-
console.log(" Press Ctrl+C to exit.");
|
|
3963
|
-
console.log("");
|
|
3964
|
-
while (true) {
|
|
3965
|
-
await this.sleep(15e3);
|
|
3966
|
-
const balance = await publicClientInstance.getBalance({
|
|
3967
|
-
address: this.client.address
|
|
3968
|
-
});
|
|
3969
|
-
if (balance > BigInt(0)) {
|
|
3970
|
-
console.log(" ETH detected! Retrying config update...");
|
|
3971
|
-
console.log("");
|
|
4311
|
+
if (balance > BigInt(0)) {
|
|
4312
|
+
console.log(" ETH detected! Retrying config update...");
|
|
4313
|
+
console.log("");
|
|
4314
|
+
try {
|
|
3972
4315
|
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
4316
|
+
console.log(`Config updated on-chain`);
|
|
4317
|
+
this.pendingConfigHash = null;
|
|
4318
|
+
} catch (retryError) {
|
|
4319
|
+
const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
4320
|
+
console.warn(`Config update failed after funding: ${retryMsg}`);
|
|
4321
|
+
console.warn("Continuing with on-chain config.");
|
|
4322
|
+
this.configHash = onChainHash;
|
|
3976
4323
|
}
|
|
3977
|
-
|
|
4324
|
+
return;
|
|
3978
4325
|
}
|
|
3979
|
-
|
|
3980
|
-
console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
|
|
3981
|
-
this.configHash = onChainHash;
|
|
4326
|
+
process.stdout.write(".");
|
|
3982
4327
|
}
|
|
4328
|
+
} else {
|
|
4329
|
+
console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
|
|
4330
|
+
this.configHash = onChainHash;
|
|
3983
4331
|
}
|
|
3984
|
-
} else {
|
|
3985
|
-
const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
3986
|
-
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
3987
4332
|
}
|
|
3988
4333
|
}
|
|
3989
4334
|
/**
|
|
@@ -4117,6 +4462,10 @@ var AgentRuntime = class {
|
|
|
4117
4462
|
}
|
|
4118
4463
|
if (updated) {
|
|
4119
4464
|
this.riskManager = new RiskManager(this.config.trading);
|
|
4465
|
+
const savedRiskState = this.positionTracker.getRiskState();
|
|
4466
|
+
if (savedRiskState.lastResetDate) {
|
|
4467
|
+
this.riskManager.restoreState(savedRiskState);
|
|
4468
|
+
}
|
|
4120
4469
|
console.log("Risk params updated via command center");
|
|
4121
4470
|
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
4122
4471
|
this.relay?.sendMessage(
|
|
@@ -4310,16 +4659,44 @@ var AgentRuntime = class {
|
|
|
4310
4659
|
this.relay?.sendCommandResult(cmd.id, false, message);
|
|
4311
4660
|
}
|
|
4312
4661
|
}
|
|
4662
|
+
/**
|
|
4663
|
+
* Periodically check if the owner has approved the pending config hash.
|
|
4664
|
+
* Called from sendRelayStatus at most every 2.5 minutes (timestamp-throttled).
|
|
4665
|
+
*/
|
|
4666
|
+
async checkPendingConfigApproval() {
|
|
4667
|
+
if (!this.pendingConfigHash) return;
|
|
4668
|
+
try {
|
|
4669
|
+
const onChain = await this.client.registry.getConfigHash(BigInt(this.config.agentId));
|
|
4670
|
+
if (onChain === this.pendingConfigHash) {
|
|
4671
|
+
this.configHash = this.pendingConfigHash;
|
|
4672
|
+
this.pendingConfigHash = null;
|
|
4673
|
+
console.log("Config verified on-chain! Your agent will now appear on the leaderboard.");
|
|
4674
|
+
this.relay?.sendMessage(
|
|
4675
|
+
"config_updated",
|
|
4676
|
+
"success",
|
|
4677
|
+
"Config Verified",
|
|
4678
|
+
"Your LLM config has been verified on-chain. Your agent will now appear on the leaderboard."
|
|
4679
|
+
);
|
|
4680
|
+
}
|
|
4681
|
+
} catch {
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4313
4684
|
/**
|
|
4314
4685
|
* Send current status to the relay
|
|
4315
4686
|
*/
|
|
4316
4687
|
sendRelayStatus() {
|
|
4317
4688
|
if (!this.relay) return;
|
|
4689
|
+
const CONFIG_CHECK_INTERVAL_MS = 15e4;
|
|
4690
|
+
if (this.pendingConfigHash && Date.now() - this.lastConfigCheckAt >= CONFIG_CHECK_INTERVAL_MS) {
|
|
4691
|
+
this.lastConfigCheckAt = Date.now();
|
|
4692
|
+
this.checkPendingConfigApproval();
|
|
4693
|
+
}
|
|
4318
4694
|
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
4319
4695
|
const status = {
|
|
4320
4696
|
mode: this.mode,
|
|
4321
4697
|
agentId: String(this.config.agentId),
|
|
4322
4698
|
wallet: this.client?.address,
|
|
4699
|
+
sdkVersion: AGENT_VERSION,
|
|
4323
4700
|
cycleCount: this.cycleCount,
|
|
4324
4701
|
lastCycleAt: this.lastCycleAt,
|
|
4325
4702
|
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
@@ -4342,30 +4719,32 @@ var AgentRuntime = class {
|
|
|
4342
4719
|
perp: this.perpConnected ? {
|
|
4343
4720
|
enabled: true,
|
|
4344
4721
|
trading: this.perpTradingActive,
|
|
4345
|
-
equity:
|
|
4346
|
-
unrealizedPnl:
|
|
4347
|
-
marginUsed:
|
|
4348
|
-
openPositions:
|
|
4349
|
-
effectiveLeverage:
|
|
4722
|
+
equity: this.cachedPerpEquity,
|
|
4723
|
+
unrealizedPnl: this.cachedPerpUnrealizedPnl,
|
|
4724
|
+
marginUsed: this.cachedPerpMarginUsed,
|
|
4725
|
+
openPositions: this.cachedPerpOpenPositions,
|
|
4726
|
+
effectiveLeverage: this.cachedPerpLeverage,
|
|
4350
4727
|
pendingRecords: this.perpRecorder?.pendingRetries ?? 0
|
|
4351
|
-
} : void 0
|
|
4728
|
+
} : void 0,
|
|
4729
|
+
positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0,
|
|
4730
|
+
configHash: this.configHash || void 0,
|
|
4731
|
+
pendingConfigHash: this.pendingConfigHash
|
|
4732
|
+
// null preserved by JSON.stringify for clearing
|
|
4352
4733
|
};
|
|
4353
|
-
|
|
4734
|
+
this.relay.sendHeartbeat(status);
|
|
4735
|
+
if (this.perpConnected && this.perpPositions) {
|
|
4354
4736
|
this.perpPositions.getAccountSummary().then((account) => {
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
status.perp.effectiveLeverage = account.effectiveLeverage;
|
|
4360
|
-
}
|
|
4737
|
+
this.cachedPerpEquity = account.totalEquity;
|
|
4738
|
+
this.cachedPerpUnrealizedPnl = account.totalUnrealizedPnl;
|
|
4739
|
+
this.cachedPerpMarginUsed = account.totalMarginUsed;
|
|
4740
|
+
this.cachedPerpLeverage = account.effectiveLeverage;
|
|
4361
4741
|
}).catch(() => {
|
|
4362
4742
|
});
|
|
4363
4743
|
this.perpPositions.getPositionCount().then((count) => {
|
|
4364
|
-
|
|
4744
|
+
this.cachedPerpOpenPositions = count;
|
|
4365
4745
|
}).catch(() => {
|
|
4366
4746
|
});
|
|
4367
4747
|
}
|
|
4368
|
-
this.relay.sendHeartbeat(status);
|
|
4369
4748
|
}
|
|
4370
4749
|
/**
|
|
4371
4750
|
* Run a single trading cycle
|
|
@@ -4379,14 +4758,19 @@ var AgentRuntime = class {
|
|
|
4379
4758
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
4380
4759
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
4381
4760
|
this.lastPortfolioValue = marketData.portfolioValue;
|
|
4761
|
+
this.lastPrices = marketData.prices;
|
|
4382
4762
|
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4383
4763
|
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
4764
|
+
this.positionTracker.syncBalances(marketData.balances, marketData.prices);
|
|
4384
4765
|
const fundsOk = this.checkFundsLow(marketData);
|
|
4385
4766
|
if (!fundsOk) {
|
|
4386
4767
|
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
4387
4768
|
this.sendRelayStatus();
|
|
4388
4769
|
return;
|
|
4389
4770
|
}
|
|
4771
|
+
this.strategyContext.positions = this.positionTracker.getPositions();
|
|
4772
|
+
this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
|
|
4773
|
+
this.strategyContext.positionTracker = this.positionTracker;
|
|
4390
4774
|
let signals;
|
|
4391
4775
|
try {
|
|
4392
4776
|
signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
|
|
@@ -4452,13 +4836,30 @@ var AgentRuntime = class {
|
|
|
4452
4836
|
);
|
|
4453
4837
|
}
|
|
4454
4838
|
}
|
|
4839
|
+
for (const result of results) {
|
|
4840
|
+
const tokenIn = result.signal.tokenIn.toLowerCase();
|
|
4841
|
+
const tokenOut = result.signal.tokenOut.toLowerCase();
|
|
4842
|
+
this.positionTracker.recordTrade({
|
|
4843
|
+
action: result.signal.action,
|
|
4844
|
+
tokenIn,
|
|
4845
|
+
tokenOut,
|
|
4846
|
+
amountIn: result.signal.amountIn,
|
|
4847
|
+
priceIn: marketData.prices[tokenIn] || 0,
|
|
4848
|
+
priceOut: marketData.prices[tokenOut] || 0,
|
|
4849
|
+
txHash: result.txHash,
|
|
4850
|
+
reasoning: result.signal.reasoning,
|
|
4851
|
+
success: result.success
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4455
4854
|
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
4456
4855
|
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
4457
4856
|
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
4458
4857
|
this.riskManager.updatePnL(marketPnL);
|
|
4858
|
+
this.positionTracker.saveRiskState(this.riskManager.exportState());
|
|
4459
4859
|
if (marketPnL !== 0) {
|
|
4460
4860
|
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
4461
4861
|
}
|
|
4862
|
+
this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
|
|
4462
4863
|
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
4463
4864
|
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
4464
4865
|
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
@@ -4914,8 +5315,12 @@ function loadSecureEnv(basePath, passphrase) {
|
|
|
4914
5315
|
}
|
|
4915
5316
|
return false;
|
|
4916
5317
|
}
|
|
5318
|
+
|
|
5319
|
+
// src/index.ts
|
|
5320
|
+
var AGENT_VERSION = "0.1.22";
|
|
4917
5321
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4918
5322
|
0 && (module.exports = {
|
|
5323
|
+
AGENT_VERSION,
|
|
4919
5324
|
AgentConfigSchema,
|
|
4920
5325
|
AgentRuntime,
|
|
4921
5326
|
AnthropicAdapter,
|
|
@@ -4939,6 +5344,7 @@ function loadSecureEnv(basePath, passphrase) {
|
|
|
4939
5344
|
PerpOnboarding,
|
|
4940
5345
|
PerpTradeRecorder,
|
|
4941
5346
|
PositionManager,
|
|
5347
|
+
PositionTracker,
|
|
4942
5348
|
RelayClient,
|
|
4943
5349
|
RelayConfigSchema,
|
|
4944
5350
|
RiskManager,
|