@exagent/agent 0.1.46 → 0.1.48
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-6VBJT53Q.mjs +9838 -0
- package/dist/chunk-CVRY5TKR.mjs +10261 -0
- package/dist/chunk-D22X7J5B.mjs +10259 -0
- package/dist/chunk-ICTDJ7VX.mjs +9879 -0
- package/dist/chunk-PNDPQ22E.mjs +9879 -0
- package/dist/chunk-SS7WXSW4.mjs +10138 -0
- package/dist/chunk-UVIDQKP4.mjs +10232 -0
- package/dist/chunk-V5XCNJWM.mjs +10261 -0
- package/dist/chunk-XRLY7P6A.mjs +10134 -0
- package/dist/chunk-YUBAS5YE.mjs +10195 -0
- package/dist/cli.js +695 -92
- package/dist/cli.mjs +91 -1
- package/dist/index.d.mts +41 -18
- package/dist/index.d.ts +41 -18
- package/dist/index.js +602 -91
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -332,7 +332,7 @@ function getTokenDecimals(address) {
|
|
|
332
332
|
function getTokenSymbol(address) {
|
|
333
333
|
return _globalResolver?.getSymbol(address.toLowerCase());
|
|
334
334
|
}
|
|
335
|
-
var import_viem2, import_chains, NATIVE_ETH, _globalResolver, TOKEN_DECIMALS, TOKEN_TO_COINGECKO, STABLECOIN_IDS, PRICE_STALENESS_MS, MarketDataService;
|
|
335
|
+
var import_viem2, import_chains, NATIVE_ETH, _globalResolver, TOKEN_DECIMALS, TOKEN_TO_COINGECKO, STABLECOIN_IDS, PRICE_STALENESS_MS, HISTORY_STALENESS_MS, MarketDataService;
|
|
336
336
|
var init_market = __esm({
|
|
337
337
|
"src/trading/market.ts"() {
|
|
338
338
|
"use strict";
|
|
@@ -498,6 +498,7 @@ var init_market = __esm({
|
|
|
498
498
|
};
|
|
499
499
|
STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai", "tether"]);
|
|
500
500
|
PRICE_STALENESS_MS = 6e4;
|
|
501
|
+
HISTORY_STALENESS_MS = 60 * 6e4;
|
|
501
502
|
MarketDataService = class {
|
|
502
503
|
rpcUrl;
|
|
503
504
|
client;
|
|
@@ -510,6 +511,10 @@ var init_market = __esm({
|
|
|
510
511
|
cachedVolume24h = {};
|
|
511
512
|
/** Cached price change data */
|
|
512
513
|
cachedPriceChange24h = {};
|
|
514
|
+
/** Cached 24h hourly price history per token address */
|
|
515
|
+
cachedPriceHistory = {};
|
|
516
|
+
/** Timestamp of last successful price history fetch */
|
|
517
|
+
lastHistoryFetchAt = 0;
|
|
513
518
|
constructor(rpcUrl, store) {
|
|
514
519
|
this.rpcUrl = rpcUrl;
|
|
515
520
|
this.client = (0, import_viem2.createPublicClient)({
|
|
@@ -530,11 +535,10 @@ var init_market = __esm({
|
|
|
530
535
|
const prices = await this.fetchPrices(tokenAddresses);
|
|
531
536
|
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
532
537
|
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
+
const [priceHistory, gasPrice] = await Promise.all([
|
|
539
|
+
this.fetchPriceHistory(tokenAddresses).catch(() => void 0),
|
|
540
|
+
this.client.getGasPrice().catch(() => void 0)
|
|
541
|
+
]);
|
|
538
542
|
return {
|
|
539
543
|
timestamp: Date.now(),
|
|
540
544
|
prices,
|
|
@@ -545,7 +549,8 @@ var init_market = __esm({
|
|
|
545
549
|
gasPrice,
|
|
546
550
|
network: {
|
|
547
551
|
chainId: this.client.chain?.id ?? 8453
|
|
548
|
-
}
|
|
552
|
+
},
|
|
553
|
+
priceHistory: priceHistory && Object.keys(priceHistory).length > 0 ? priceHistory : void 0
|
|
549
554
|
};
|
|
550
555
|
}
|
|
551
556
|
/**
|
|
@@ -718,6 +723,58 @@ var init_market = __esm({
|
|
|
718
723
|
}
|
|
719
724
|
return prices;
|
|
720
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Fetch 24h hourly price history from CoinGecko.
|
|
728
|
+
* Returns cached data if still fresh (< 60 min old).
|
|
729
|
+
* Only fetches for tokens with known CoinGecko IDs.
|
|
730
|
+
*/
|
|
731
|
+
async fetchPriceHistory(tokenAddresses) {
|
|
732
|
+
if (Date.now() - this.lastHistoryFetchAt < HISTORY_STALENESS_MS && Object.keys(this.cachedPriceHistory).length > 0) {
|
|
733
|
+
return { ...this.cachedPriceHistory };
|
|
734
|
+
}
|
|
735
|
+
const history = {};
|
|
736
|
+
const idToAddrs = {};
|
|
737
|
+
for (const addr of tokenAddresses) {
|
|
738
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
739
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
740
|
+
if (!idToAddrs[cgId]) idToAddrs[cgId] = [];
|
|
741
|
+
idToAddrs[cgId].push(addr.toLowerCase());
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const ids = Object.keys(idToAddrs);
|
|
745
|
+
for (let i = 0; i < ids.length; i++) {
|
|
746
|
+
const cgId = ids[i];
|
|
747
|
+
try {
|
|
748
|
+
const resp = await fetch(
|
|
749
|
+
`https://api.coingecko.com/api/v3/coins/${cgId}/market_chart?vs_currency=usd&days=1`,
|
|
750
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
751
|
+
);
|
|
752
|
+
if (!resp.ok) {
|
|
753
|
+
if (resp.status === 429) {
|
|
754
|
+
console.warn("CoinGecko rate limit hit during price history fetch \u2014 using partial data");
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const data = await resp.json();
|
|
760
|
+
if (data.prices && Array.isArray(data.prices)) {
|
|
761
|
+
const candles = data.prices.map(([ts, price]) => ({ timestamp: ts, price }));
|
|
762
|
+
for (const addr of idToAddrs[cgId]) {
|
|
763
|
+
history[addr] = candles;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
if (i < ids.length - 1) {
|
|
769
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (Object.keys(history).length > 0) {
|
|
773
|
+
this.cachedPriceHistory = { ...this.cachedPriceHistory, ...history };
|
|
774
|
+
this.lastHistoryFetchAt = Date.now();
|
|
775
|
+
}
|
|
776
|
+
return { ...this.cachedPriceHistory };
|
|
777
|
+
}
|
|
721
778
|
/**
|
|
722
779
|
* Fetch real on-chain balances: native ETH + ERC-20 tokens.
|
|
723
780
|
* Uses Multicall3 to batch all balanceOf calls into a single RPC request.
|
|
@@ -1552,16 +1609,25 @@ var init_executor = __esm({
|
|
|
1552
1609
|
config;
|
|
1553
1610
|
allowedTokens;
|
|
1554
1611
|
configHashFn;
|
|
1555
|
-
|
|
1612
|
+
vaultManager;
|
|
1613
|
+
constructor(client, config, configHashFn, vaultManager) {
|
|
1556
1614
|
this.client = client;
|
|
1557
1615
|
this.config = config;
|
|
1558
1616
|
this.configHashFn = configHashFn;
|
|
1617
|
+
this.vaultManager = vaultManager;
|
|
1559
1618
|
this.allowedTokens = new Set(
|
|
1560
1619
|
(config.allowedTokens || []).map((t) => t.toLowerCase())
|
|
1561
1620
|
);
|
|
1562
1621
|
}
|
|
1563
1622
|
/**
|
|
1564
|
-
*
|
|
1623
|
+
* Set the vault manager (allows wiring after construction)
|
|
1624
|
+
*/
|
|
1625
|
+
setVaultManager(vm) {
|
|
1626
|
+
this.vaultManager = vm;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Execute a single trade signal.
|
|
1630
|
+
* Tries vault trading first if vault exists, falls back to wallet trading.
|
|
1565
1631
|
*/
|
|
1566
1632
|
async execute(signal) {
|
|
1567
1633
|
if (signal.action === "hold") {
|
|
@@ -1574,13 +1640,38 @@ var init_executor = __esm({
|
|
|
1574
1640
|
return { success: false, error: "Signal exceeds position limits" };
|
|
1575
1641
|
}
|
|
1576
1642
|
const configHash = this.configHashFn?.();
|
|
1577
|
-
const
|
|
1643
|
+
const tradeIntent = {
|
|
1578
1644
|
tokenIn: signal.tokenIn,
|
|
1579
1645
|
tokenOut: signal.tokenOut,
|
|
1580
1646
|
amountIn: signal.amountIn,
|
|
1581
1647
|
maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
|
|
1582
1648
|
...configHash && { configHash }
|
|
1583
|
-
}
|
|
1649
|
+
};
|
|
1650
|
+
if (this.vaultManager) {
|
|
1651
|
+
const vaultAddress = await this.vaultManager.getVaultAddress();
|
|
1652
|
+
if (vaultAddress && this.vaultManager.preferVaultTrading) {
|
|
1653
|
+
const routerTrade = await this.client.buildRouterTrade(tradeIntent);
|
|
1654
|
+
if (routerTrade.vaultParams) {
|
|
1655
|
+
console.log(`Attempting vault trade via ${vaultAddress}`);
|
|
1656
|
+
const vaultResult = await this.vaultManager.executeVaultTrade({
|
|
1657
|
+
tokenIn: signal.tokenIn,
|
|
1658
|
+
tokenOut: signal.tokenOut,
|
|
1659
|
+
amountIn: BigInt(signal.amountIn),
|
|
1660
|
+
minAmountOut: BigInt(routerTrade.minAmountOut),
|
|
1661
|
+
vaultParams: routerTrade.vaultParams
|
|
1662
|
+
});
|
|
1663
|
+
if (vaultResult) {
|
|
1664
|
+
if (vaultResult.txHash) {
|
|
1665
|
+
console.log(`Vault trade executed: ${vaultResult.txHash}`);
|
|
1666
|
+
return { success: true, txHash: vaultResult.txHash };
|
|
1667
|
+
}
|
|
1668
|
+
console.error(`Vault trade failed: ${vaultResult.error}`);
|
|
1669
|
+
return { success: false, error: vaultResult.error };
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const result = await this.client.trade(tradeIntent);
|
|
1584
1675
|
console.log(`Trade executed: ${result.hash}`);
|
|
1585
1676
|
return { success: true, txHash: result.hash };
|
|
1586
1677
|
} catch (error) {
|
|
@@ -1609,6 +1700,10 @@ var init_executor = __esm({
|
|
|
1609
1700
|
* Validate a signal against config limits and token restrictions
|
|
1610
1701
|
*/
|
|
1611
1702
|
validateSignal(signal) {
|
|
1703
|
+
if (signal.amountIn <= 0n) {
|
|
1704
|
+
console.warn(`Signal amountIn is ${signal.amountIn} \u2014 skipping (must be positive)`);
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1612
1707
|
if (signal.confidence < 0.5) {
|
|
1613
1708
|
console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
|
|
1614
1709
|
return false;
|
|
@@ -2553,6 +2648,33 @@ function formatSessionReport(result) {
|
|
|
2553
2648
|
}
|
|
2554
2649
|
lines.push("");
|
|
2555
2650
|
}
|
|
2651
|
+
if (result.trades.length > 0) {
|
|
2652
|
+
lines.push(` ${thinDivider}`);
|
|
2653
|
+
lines.push(" TRADE LOG");
|
|
2654
|
+
lines.push(` ${thinDivider}`);
|
|
2655
|
+
lines.push("");
|
|
2656
|
+
const recentTrades = result.trades.slice(-50);
|
|
2657
|
+
for (const t of recentTrades) {
|
|
2658
|
+
const time = new Date(t.timestamp).toLocaleString(void 0, {
|
|
2659
|
+
month: "short",
|
|
2660
|
+
day: "2-digit",
|
|
2661
|
+
hour: "2-digit",
|
|
2662
|
+
minute: "2-digit"
|
|
2663
|
+
});
|
|
2664
|
+
const tag = t.action === "buy" ? "BUY " : "SELL";
|
|
2665
|
+
const tokenIn = t.tokenIn.slice(0, 6) + "\u2026";
|
|
2666
|
+
const tokenOut = t.tokenOut.slice(0, 6) + "\u2026";
|
|
2667
|
+
const value = `$${t.valueInUSD.toFixed(2)}`;
|
|
2668
|
+
const pnl = t.valueOutUSD - t.valueInUSD - t.feeUSD - t.gasUSD;
|
|
2669
|
+
const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}`;
|
|
2670
|
+
const reason = t.reasoning ? ` "${t.reasoning.slice(0, 60)}"` : "";
|
|
2671
|
+
lines.push(` ${time} ${tag} ${tokenIn}\u2192${tokenOut} ${value} ${pnlStr}${reason}`);
|
|
2672
|
+
}
|
|
2673
|
+
if (result.trades.length > 50) {
|
|
2674
|
+
lines.push(` ... and ${result.trades.length - 50} earlier trades`);
|
|
2675
|
+
}
|
|
2676
|
+
lines.push("");
|
|
2677
|
+
}
|
|
2556
2678
|
if (result.equityCurve.length >= 3) {
|
|
2557
2679
|
lines.push(` ${thinDivider}`);
|
|
2558
2680
|
lines.push(" EQUITY CURVE");
|
|
@@ -3886,7 +4008,7 @@ var STRATEGY_TEMPLATES = [
|
|
|
3886
4008
|
{
|
|
3887
4009
|
id: "momentum",
|
|
3888
4010
|
name: "Momentum Trader",
|
|
3889
|
-
description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum.",
|
|
4011
|
+
description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum, sells when momentum fades.",
|
|
3890
4012
|
riskLevel: "medium",
|
|
3891
4013
|
riskWarnings: [
|
|
3892
4014
|
"Momentum strategies can suffer significant losses during trend reversals",
|
|
@@ -3894,60 +4016,248 @@ var STRATEGY_TEMPLATES = [
|
|
|
3894
4016
|
"Past performance does not guarantee future results",
|
|
3895
4017
|
"This strategy may underperform in sideways markets"
|
|
3896
4018
|
],
|
|
3897
|
-
systemPrompt: `You are an AI trading analyst specializing in momentum trading
|
|
4019
|
+
systemPrompt: `You are an AI trading analyst specializing in momentum trading on Base network.
|
|
3898
4020
|
|
|
3899
|
-
|
|
4021
|
+
Analyze the provided market data and identify momentum-based trading opportunities.
|
|
3900
4022
|
|
|
3901
|
-
|
|
4023
|
+
RULES:
|
|
3902
4024
|
- Only recommend trades when there is clear momentum evidence
|
|
3903
|
-
-
|
|
3904
|
-
-
|
|
3905
|
-
- Be conservative with confidence
|
|
4025
|
+
- Use priceTrends (1h, 4h, 12h, 24h changes) as your primary momentum signal
|
|
4026
|
+
- Fall back to priceChange24h if priceTrends is not available
|
|
4027
|
+
- Be conservative with confidence \u2014 only use > 0.6 for strong signals
|
|
4028
|
+
- Never recommend buying a token you already hold significant amounts of
|
|
4029
|
+
- Always consider selling positions that have lost momentum
|
|
4030
|
+
- Return "hold" signals when uncertain \u2014 doing nothing is often the best trade
|
|
3906
4031
|
|
|
3907
|
-
|
|
3908
|
-
1.
|
|
3909
|
-
2.
|
|
3910
|
-
3.
|
|
4032
|
+
ANALYZE:
|
|
4033
|
+
1. Multi-timeframe momentum: use priceTrends (1h, 4h, 12h, 24h) to confirm trend direction
|
|
4034
|
+
2. Strong momentum = positive across all timeframes. Divergence (e.g., 1h negative, 12h positive) = weakening
|
|
4035
|
+
3. Volume confirmation: prefer tokens with > $10K daily volume
|
|
4036
|
+
4. Portfolio concentration: avoid putting > 20% in any single token
|
|
4037
|
+
5. Exit signals: sell when short-term trend (1h, 4h) turns negative on a held position
|
|
3911
4038
|
|
|
3912
|
-
Respond with JSON
|
|
4039
|
+
Respond with ONLY valid JSON (no markdown, no code fences):
|
|
3913
4040
|
{
|
|
3914
|
-
"analysis": "Brief market analysis",
|
|
4041
|
+
"analysis": "Brief market analysis (1-2 sentences)",
|
|
3915
4042
|
"signals": [
|
|
3916
4043
|
{
|
|
3917
4044
|
"action": "buy" | "sell" | "hold",
|
|
3918
|
-
"
|
|
3919
|
-
"
|
|
3920
|
-
"
|
|
3921
|
-
"
|
|
3922
|
-
"reasoning": "Why this trade"
|
|
4045
|
+
"token": "0x... (token address to buy/sell)",
|
|
4046
|
+
"percentage": 5-25,
|
|
4047
|
+
"confidence": 0.0-1.0,
|
|
4048
|
+
"reasoning": "Why this trade (1 sentence)"
|
|
3923
4049
|
}
|
|
3924
4050
|
]
|
|
3925
|
-
}
|
|
3926
|
-
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
If no good opportunities exist, return: { "analysis": "No clear momentum signals", "signals": [] }`,
|
|
4054
|
+
exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
|
|
4055
|
+
|
|
4056
|
+
// Well-known token addresses on Base
|
|
4057
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
4058
|
+
const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
|
|
3927
4059
|
|
|
4060
|
+
// Token decimals lookup (extend as needed)
|
|
4061
|
+
const DECIMALS: Record<string, number> = {
|
|
4062
|
+
[WETH]: 18,
|
|
4063
|
+
[USDC]: 6,
|
|
4064
|
+
};
|
|
4065
|
+
|
|
4066
|
+
/**
|
|
4067
|
+
* Format balances for LLM consumption (human-readable amounts)
|
|
4068
|
+
*/
|
|
4069
|
+
function formatBalances(balances: Record<string, bigint>, prices: Record<string, number>): Record<string, { amount: string; usdValue: string }> {
|
|
4070
|
+
const result: Record<string, { amount: string; usdValue: string }> = {};
|
|
4071
|
+
for (const [token, balance] of Object.entries(balances)) {
|
|
4072
|
+
if (balance === 0n) continue;
|
|
4073
|
+
const decimals = DECIMALS[token.toLowerCase()] ?? 18;
|
|
4074
|
+
const amount = Number(balance) / (10 ** decimals);
|
|
4075
|
+
const price = prices[token.toLowerCase()] ?? 0;
|
|
4076
|
+
result[token] = {
|
|
4077
|
+
amount: amount.toFixed(decimals <= 8 ? decimals : 6),
|
|
4078
|
+
usdValue: (amount * price).toFixed(2),
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
return result;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
/**
|
|
4085
|
+
* Convert LLM percentage signal to a TradeSignal with bigint amountIn
|
|
4086
|
+
*
|
|
4087
|
+
* The LLM says "buy token X with 10% of my ETH" \u2014 this function converts
|
|
4088
|
+
* that percentage into the actual wei amount to trade.
|
|
4089
|
+
*/
|
|
4090
|
+
function toTradeSignal(
|
|
4091
|
+
signal: { action: string; token: string; percentage: number; confidence: number; reasoning?: string },
|
|
4092
|
+
balances: Record<string, bigint>,
|
|
4093
|
+
): TradeSignal | null {
|
|
4094
|
+
const { action, token, percentage, confidence, reasoning } = signal;
|
|
4095
|
+
|
|
4096
|
+
// Skip hold signals and low confidence
|
|
4097
|
+
if (action === 'hold' || confidence < 0.5) return null;
|
|
4098
|
+
|
|
4099
|
+
// Clamp percentage to 1-50 range for safety
|
|
4100
|
+
const pct = Math.max(1, Math.min(50, percentage));
|
|
4101
|
+
|
|
4102
|
+
if (action === 'buy') {
|
|
4103
|
+
// Buying: spend ETH (or USDC) to acquire the target token
|
|
4104
|
+
// Use ETH balance as the source
|
|
4105
|
+
const ethBalance = balances[WETH] ?? 0n;
|
|
4106
|
+
if (ethBalance === 0n) return null;
|
|
4107
|
+
const amountIn = (ethBalance * BigInt(Math.round(pct * 100))) / 10000n;
|
|
4108
|
+
if (amountIn === 0n) return null;
|
|
4109
|
+
return { action: 'buy', tokenIn: WETH, tokenOut: token, amountIn, confidence, reasoning };
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
if (action === 'sell') {
|
|
4113
|
+
// Selling: sell the target token for ETH
|
|
4114
|
+
const tokenBalance = balances[token.toLowerCase()] ?? balances[token] ?? 0n;
|
|
4115
|
+
if (tokenBalance === 0n) return null;
|
|
4116
|
+
const amountIn = (tokenBalance * BigInt(Math.round(pct * 100))) / 10000n;
|
|
4117
|
+
if (amountIn === 0n) return null;
|
|
4118
|
+
return { action: 'sell', tokenIn: token, tokenOut: WETH, amountIn, confidence, reasoning };
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
return null;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
/**
|
|
4125
|
+
* Momentum Trading Strategy
|
|
4126
|
+
*
|
|
4127
|
+
* Sends market data to the LLM, parses the response, and converts
|
|
4128
|
+
* percentage-based signals into executable TradeSignal objects.
|
|
4129
|
+
*/
|
|
3928
4130
|
export const generateSignals: StrategyFunction = async (
|
|
3929
4131
|
marketData: MarketData,
|
|
3930
4132
|
llm: LLMAdapter,
|
|
3931
|
-
config: AgentConfig
|
|
4133
|
+
config: AgentConfig,
|
|
4134
|
+
context?: StrategyContext,
|
|
3932
4135
|
): Promise<TradeSignal[]> => {
|
|
4136
|
+
// Summarize price history into compact trend data for the LLM
|
|
4137
|
+
// Instead of sending 24 raw candles per token, compute useful signals:
|
|
4138
|
+
// - 1h, 4h, 12h, 24h price changes (momentum at different timeframes)
|
|
4139
|
+
const priceTrends: Record<string, { '1h': string; '4h': string; '12h': string; '24h': string }> = {};
|
|
4140
|
+
if (marketData.priceHistory) {
|
|
4141
|
+
for (const [addr, candles] of Object.entries(marketData.priceHistory)) {
|
|
4142
|
+
if (candles.length < 2) continue;
|
|
4143
|
+
const latest = candles[candles.length - 1].price;
|
|
4144
|
+
const getChangeAt = (hoursAgo: number): string => {
|
|
4145
|
+
const target = Date.now() - hoursAgo * 3600_000;
|
|
4146
|
+
// Find the candle closest to the target time
|
|
4147
|
+
let closest = candles[0];
|
|
4148
|
+
for (const c of candles) {
|
|
4149
|
+
if (Math.abs(c.timestamp - target) < Math.abs(closest.timestamp - target)) closest = c;
|
|
4150
|
+
}
|
|
4151
|
+
if (closest.price === 0) return '0.0%';
|
|
4152
|
+
const pct = ((latest - closest.price) / closest.price) * 100;
|
|
4153
|
+
return (pct >= 0 ? '+' : '') + pct.toFixed(1) + '%';
|
|
4154
|
+
};
|
|
4155
|
+
priceTrends[addr] = {
|
|
4156
|
+
'1h': getChangeAt(1),
|
|
4157
|
+
'4h': getChangeAt(4),
|
|
4158
|
+
'12h': getChangeAt(12),
|
|
4159
|
+
'24h': getChangeAt(24),
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// Build the data payload for the LLM
|
|
4165
|
+
const payload: Record<string, unknown> = {
|
|
4166
|
+
prices: marketData.prices,
|
|
4167
|
+
balances: formatBalances(marketData.balances, marketData.prices),
|
|
4168
|
+
portfolioValueUSD: marketData.portfolioValue.toFixed(2),
|
|
4169
|
+
priceChange24h: marketData.priceChange24h ?? {},
|
|
4170
|
+
volume24h: marketData.volume24h ?? {},
|
|
4171
|
+
recentTrades: (context?.tradeHistory ?? []).slice(0, 5).map(t => ({
|
|
4172
|
+
action: t.action,
|
|
4173
|
+
token: t.action === 'buy' ? t.tokenOut : t.tokenIn,
|
|
4174
|
+
reasoning: t.reasoning,
|
|
4175
|
+
timestamp: new Date(t.timestamp).toISOString(),
|
|
4176
|
+
})),
|
|
4177
|
+
};
|
|
4178
|
+
|
|
4179
|
+
// Include price trends if available (more useful than raw candles for LLM)
|
|
4180
|
+
if (Object.keys(priceTrends).length > 0) {
|
|
4181
|
+
payload.priceTrends = priceTrends;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// Call the LLM
|
|
3933
4185
|
const response = await llm.chat([
|
|
3934
|
-
{ role: 'system', content: MOMENTUM_SYSTEM_PROMPT },
|
|
3935
|
-
{ role: 'user', content: JSON.stringify(
|
|
3936
|
-
prices: marketData.prices,
|
|
3937
|
-
balances: formatBalances(marketData.balances),
|
|
3938
|
-
portfolioValue: marketData.portfolioValue,
|
|
3939
|
-
})}
|
|
4186
|
+
{ role: 'system', content: config.strategyPrompt ?? MOMENTUM_SYSTEM_PROMPT },
|
|
4187
|
+
{ role: 'user', content: JSON.stringify(payload) },
|
|
3940
4188
|
]);
|
|
3941
4189
|
|
|
3942
|
-
// Parse
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
4190
|
+
// Parse the response \u2014 handle markdown code fences if the LLM wraps its output
|
|
4191
|
+
let content = response.content.trim();
|
|
4192
|
+
if (content.startsWith('\`\`\`')) {
|
|
4193
|
+
content = content.replace(/^\`\`\`(?:json)?\\n?/, '').replace(/\\n?\`\`\`$/, '').trim();
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
let parsed: { signals?: Array<{ action: string; token: string; percentage: number; confidence: number; reasoning?: string }> };
|
|
4197
|
+
try {
|
|
4198
|
+
parsed = JSON.parse(content);
|
|
4199
|
+
} catch (e) {
|
|
4200
|
+
console.error('[strategy] Failed to parse LLM response:', (e as Error).message);
|
|
4201
|
+
console.error('[strategy] Raw response:', content.slice(0, 200));
|
|
4202
|
+
return []; // Safe fallback: no trades
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
if (!parsed.signals || !Array.isArray(parsed.signals)) {
|
|
4206
|
+
return [];
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// Convert each signal to a TradeSignal with proper bigint amountIn
|
|
4210
|
+
const signals: TradeSignal[] = [];
|
|
4211
|
+
for (const raw of parsed.signals) {
|
|
4212
|
+
const signal = toTradeSignal(raw, marketData.balances);
|
|
4213
|
+
if (signal) signals.push(signal);
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
return signals;
|
|
4217
|
+
};
|
|
4218
|
+
|
|
4219
|
+
// Default system prompt \u2014 used when config.strategyPrompt is not set
|
|
4220
|
+
const MOMENTUM_SYSTEM_PROMPT = \`You are an AI trading analyst specializing in momentum trading on Base network.
|
|
4221
|
+
|
|
4222
|
+
Analyze the provided market data and identify momentum-based trading opportunities.
|
|
4223
|
+
|
|
4224
|
+
RULES:
|
|
4225
|
+
- Only recommend trades when there is clear momentum evidence
|
|
4226
|
+
- Use priceTrends (1h, 4h, 12h, 24h changes) as your primary momentum signal
|
|
4227
|
+
- Fall back to priceChange24h if priceTrends is not available
|
|
4228
|
+
- Be conservative with confidence \u2014 only use > 0.6 for strong signals
|
|
4229
|
+
- Never recommend buying a token you already hold significant amounts of
|
|
4230
|
+
- Always consider selling positions that have lost momentum
|
|
4231
|
+
- Return "hold" signals when uncertain \u2014 doing nothing is often the best trade
|
|
4232
|
+
|
|
4233
|
+
ANALYZE:
|
|
4234
|
+
1. Multi-timeframe momentum: use priceTrends (1h, 4h, 12h, 24h) to confirm trend direction
|
|
4235
|
+
2. Strong momentum = positive across all timeframes. Divergence (e.g., 1h negative, 12h positive) = weakening
|
|
4236
|
+
3. Volume confirmation: prefer tokens with > $10K daily volume
|
|
4237
|
+
4. Portfolio concentration: avoid putting > 20% in any single token
|
|
4238
|
+
5. Exit signals: sell when short-term trend (1h, 4h) turns negative on a held position
|
|
4239
|
+
|
|
4240
|
+
Respond with ONLY valid JSON (no markdown, no code fences):
|
|
4241
|
+
{
|
|
4242
|
+
"analysis": "Brief market analysis (1-2 sentences)",
|
|
4243
|
+
"signals": [
|
|
4244
|
+
{
|
|
4245
|
+
"action": "buy" | "sell" | "hold",
|
|
4246
|
+
"token": "0x... (token address to buy/sell)",
|
|
4247
|
+
"percentage": 5-25,
|
|
4248
|
+
"confidence": 0.0-1.0,
|
|
4249
|
+
"reasoning": "Why this trade (1 sentence)"
|
|
4250
|
+
}
|
|
4251
|
+
]
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
If no good opportunities exist, return: { "analysis": "No clear momentum signals", "signals": [] }\`;
|
|
4255
|
+
`
|
|
3946
4256
|
},
|
|
3947
4257
|
{
|
|
3948
4258
|
id: "value",
|
|
3949
4259
|
name: "Value Investor",
|
|
3950
|
-
description: "Looks for undervalued assets based on fundamentals. Takes long-term positions.",
|
|
4260
|
+
description: "Looks for undervalued assets based on fundamentals. Takes long-term positions with lower turnover.",
|
|
3951
4261
|
riskLevel: "low",
|
|
3952
4262
|
riskWarnings: [
|
|
3953
4263
|
"Value traps can result in prolonged losses",
|
|
@@ -3955,47 +4265,135 @@ export const generateSignals: StrategyFunction = async (
|
|
|
3955
4265
|
"Fundamental analysis may not apply well to all crypto assets",
|
|
3956
4266
|
"Market sentiment can override fundamentals for long periods"
|
|
3957
4267
|
],
|
|
3958
|
-
systemPrompt: `You are an AI trading analyst specializing in value investing.
|
|
4268
|
+
systemPrompt: `You are an AI trading analyst specializing in value investing on Base network.
|
|
3959
4269
|
|
|
3960
4270
|
Your role is to identify undervalued assets with strong fundamentals.
|
|
3961
4271
|
|
|
3962
|
-
|
|
4272
|
+
RULES:
|
|
3963
4273
|
- Focus on long-term value, not short-term price movements
|
|
3964
4274
|
- Only recommend assets with clear value propositions
|
|
3965
4275
|
- Consider protocol revenue, TVL, active users, developer activity
|
|
3966
|
-
- Be very selective
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
1. Protocol fundamentals (revenue, TVL, user growth)
|
|
3970
|
-
2. Token economics (supply schedule, utility)
|
|
3971
|
-
3. Competitive positioning
|
|
3972
|
-
4. Valuation relative to peers
|
|
4276
|
+
- Be very selective \u2014 quality over quantity
|
|
4277
|
+
- Prefer established tokens (AERO, WELL, MORPHO, COMP, CRV) over memecoins
|
|
4278
|
+
- Hold positions for days/weeks, not hours
|
|
3973
4279
|
|
|
3974
|
-
Respond with
|
|
4280
|
+
Respond with ONLY valid JSON:
|
|
3975
4281
|
{
|
|
3976
4282
|
"analysis": "Brief fundamental analysis",
|
|
3977
4283
|
"signals": [
|
|
3978
4284
|
{
|
|
3979
4285
|
"action": "buy" | "sell" | "hold",
|
|
3980
|
-
"
|
|
3981
|
-
"
|
|
3982
|
-
"
|
|
3983
|
-
"confidence": 0-1,
|
|
4286
|
+
"token": "0x... (token address)",
|
|
4287
|
+
"percentage": 5-20,
|
|
4288
|
+
"confidence": 0.0-1.0,
|
|
3984
4289
|
"reasoning": "Fundamental thesis"
|
|
3985
4290
|
}
|
|
3986
4291
|
]
|
|
3987
4292
|
}`,
|
|
3988
|
-
exampleCode: `import { StrategyFunction } from '@exagent/agent';
|
|
4293
|
+
exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
|
|
4294
|
+
|
|
4295
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
4296
|
+
const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
|
|
4297
|
+
const DECIMALS: Record<string, number> = { [WETH]: 18, [USDC]: 6 };
|
|
4298
|
+
|
|
4299
|
+
function formatBalances(balances: Record<string, bigint>, prices: Record<string, number>): Record<string, { amount: string; usdValue: string }> {
|
|
4300
|
+
const result: Record<string, { amount: string; usdValue: string }> = {};
|
|
4301
|
+
for (const [token, balance] of Object.entries(balances)) {
|
|
4302
|
+
if (balance === 0n) continue;
|
|
4303
|
+
const decimals = DECIMALS[token.toLowerCase()] ?? 18;
|
|
4304
|
+
const amount = Number(balance) / (10 ** decimals);
|
|
4305
|
+
const price = prices[token.toLowerCase()] ?? 0;
|
|
4306
|
+
result[token] = { amount: amount.toFixed(6), usdValue: (amount * price).toFixed(2) };
|
|
4307
|
+
}
|
|
4308
|
+
return result;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function toTradeSignal(
|
|
4312
|
+
signal: { action: string; token: string; percentage: number; confidence: number; reasoning?: string },
|
|
4313
|
+
balances: Record<string, bigint>,
|
|
4314
|
+
): TradeSignal | null {
|
|
4315
|
+
if (signal.action === 'hold' || signal.confidence < 0.5) return null;
|
|
4316
|
+
const pct = Math.max(1, Math.min(50, signal.percentage));
|
|
4317
|
+
if (signal.action === 'buy') {
|
|
4318
|
+
const ethBalance = balances[WETH] ?? 0n;
|
|
4319
|
+
if (ethBalance === 0n) return null;
|
|
4320
|
+
const amountIn = (ethBalance * BigInt(Math.round(pct * 100))) / 10000n;
|
|
4321
|
+
if (amountIn === 0n) return null;
|
|
4322
|
+
return { action: 'buy', tokenIn: WETH, tokenOut: signal.token, amountIn, confidence: signal.confidence, reasoning: signal.reasoning };
|
|
4323
|
+
}
|
|
4324
|
+
if (signal.action === 'sell') {
|
|
4325
|
+
const tokenBalance = balances[signal.token.toLowerCase()] ?? balances[signal.token] ?? 0n;
|
|
4326
|
+
if (tokenBalance === 0n) return null;
|
|
4327
|
+
const amountIn = (tokenBalance * BigInt(Math.round(pct * 100))) / 10000n;
|
|
4328
|
+
if (amountIn === 0n) return null;
|
|
4329
|
+
return { action: 'sell', tokenIn: signal.token, tokenOut: WETH, amountIn, confidence: signal.confidence, reasoning: signal.reasoning };
|
|
4330
|
+
}
|
|
4331
|
+
return null;
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
export const generateSignals: StrategyFunction = async (
|
|
4335
|
+
marketData: MarketData,
|
|
4336
|
+
llm: LLMAdapter,
|
|
4337
|
+
config: AgentConfig,
|
|
4338
|
+
context?: StrategyContext,
|
|
4339
|
+
): Promise<TradeSignal[]> => {
|
|
4340
|
+
const payload = {
|
|
4341
|
+
prices: marketData.prices,
|
|
4342
|
+
balances: formatBalances(marketData.balances, marketData.prices),
|
|
4343
|
+
portfolioValueUSD: marketData.portfolioValue.toFixed(2),
|
|
4344
|
+
priceChange24h: marketData.priceChange24h ?? {},
|
|
4345
|
+
positions: (context?.positions ?? []).map(p => ({
|
|
4346
|
+
token: p.tokenAddress,
|
|
4347
|
+
entryPrice: p.averageEntryPrice,
|
|
4348
|
+
currentAmount: p.currentAmount.toString(),
|
|
4349
|
+
})),
|
|
4350
|
+
};
|
|
3989
4351
|
|
|
3990
|
-
export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
|
|
3991
|
-
// Value strategy runs less frequently
|
|
3992
4352
|
const response = await llm.chat([
|
|
3993
|
-
{ role: 'system', content: VALUE_SYSTEM_PROMPT },
|
|
3994
|
-
{ role: 'user', content: JSON.stringify(
|
|
4353
|
+
{ role: 'system', content: config.strategyPrompt ?? VALUE_SYSTEM_PROMPT },
|
|
4354
|
+
{ role: 'user', content: JSON.stringify(payload) },
|
|
3995
4355
|
]);
|
|
3996
4356
|
|
|
3997
|
-
|
|
3998
|
-
|
|
4357
|
+
let content = response.content.trim();
|
|
4358
|
+
if (content.startsWith('\\\`\\\`\\\`')) {
|
|
4359
|
+
content = content.replace(/^\\\`\\\`\\\`(?:json)?\\n?/, '').replace(/\\n?\\\`\\\`\\\`$/, '').trim();
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
try {
|
|
4363
|
+
const parsed = JSON.parse(content);
|
|
4364
|
+
if (!parsed.signals || !Array.isArray(parsed.signals)) return [];
|
|
4365
|
+
return parsed.signals.map((s: any) => toTradeSignal(s, marketData.balances)).filter(Boolean) as TradeSignal[];
|
|
4366
|
+
} catch (e) {
|
|
4367
|
+
console.error('[strategy] Failed to parse LLM response:', (e as Error).message);
|
|
4368
|
+
return [];
|
|
4369
|
+
}
|
|
4370
|
+
};
|
|
4371
|
+
|
|
4372
|
+
const VALUE_SYSTEM_PROMPT = \`You are an AI trading analyst specializing in value investing on Base network.
|
|
4373
|
+
|
|
4374
|
+
Your role is to identify undervalued assets with strong fundamentals.
|
|
4375
|
+
|
|
4376
|
+
RULES:
|
|
4377
|
+
- Focus on long-term value, not short-term price movements
|
|
4378
|
+
- Only recommend assets with clear value propositions
|
|
4379
|
+
- Be very selective \u2014 quality over quantity
|
|
4380
|
+
- Prefer established tokens over memecoins
|
|
4381
|
+
- Hold positions for days/weeks, not hours
|
|
4382
|
+
|
|
4383
|
+
Respond with ONLY valid JSON:
|
|
4384
|
+
{
|
|
4385
|
+
"analysis": "Brief fundamental analysis",
|
|
4386
|
+
"signals": [
|
|
4387
|
+
{
|
|
4388
|
+
"action": "buy" | "sell" | "hold",
|
|
4389
|
+
"token": "0x... (token address)",
|
|
4390
|
+
"percentage": 5-20,
|
|
4391
|
+
"confidence": 0.0-1.0,
|
|
4392
|
+
"reasoning": "Fundamental thesis"
|
|
4393
|
+
}
|
|
4394
|
+
]
|
|
4395
|
+
}\`;
|
|
4396
|
+
`
|
|
3999
4397
|
},
|
|
4000
4398
|
{
|
|
4001
4399
|
id: "arbitrage",
|
|
@@ -4035,7 +4433,7 @@ Respond with JSON in this format:
|
|
|
4035
4433
|
exampleCode: `// Note: Pure arbitrage requires specialized infrastructure
|
|
4036
4434
|
// This template is for educational purposes
|
|
4037
4435
|
|
|
4038
|
-
import { StrategyFunction } from '@exagent/agent';
|
|
4436
|
+
import type { StrategyFunction } from '@exagent/agent';
|
|
4039
4437
|
|
|
4040
4438
|
export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
|
|
4041
4439
|
// Arbitrage requires real-time price feeds from multiple sources
|
|
@@ -4067,18 +4465,21 @@ Respond with JSON:
|
|
|
4067
4465
|
{
|
|
4068
4466
|
"signals": []
|
|
4069
4467
|
}`,
|
|
4070
|
-
exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
|
|
4468
|
+
exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
|
|
4469
|
+
|
|
4470
|
+
const WETH = '0x4200000000000000000000000000000000000006';
|
|
4071
4471
|
|
|
4072
4472
|
/**
|
|
4073
4473
|
* Custom Strategy Template
|
|
4074
4474
|
*
|
|
4075
4475
|
* Customize this file with your own trading logic and prompts.
|
|
4076
|
-
* Your prompts are YOUR intellectual property
|
|
4476
|
+
* Your prompts are YOUR intellectual property \u2014 we don't store them.
|
|
4077
4477
|
*/
|
|
4078
4478
|
export const generateSignals: StrategyFunction = async (
|
|
4079
4479
|
marketData: MarketData,
|
|
4080
4480
|
llm: LLMAdapter,
|
|
4081
|
-
config: AgentConfig
|
|
4481
|
+
config: AgentConfig,
|
|
4482
|
+
context?: StrategyContext,
|
|
4082
4483
|
): Promise<TradeSignal[]> => {
|
|
4083
4484
|
// Your custom system prompt (this is your secret sauce)
|
|
4084
4485
|
const systemPrompt = \`
|
|
@@ -4088,13 +4489,26 @@ export const generateSignals: StrategyFunction = async (
|
|
|
4088
4489
|
// Call the LLM with your prompt
|
|
4089
4490
|
const response = await llm.chat([
|
|
4090
4491
|
{ role: 'system', content: systemPrompt },
|
|
4091
|
-
{ role: 'user', content: JSON.stringify(
|
|
4492
|
+
{ role: 'user', content: JSON.stringify({
|
|
4493
|
+
prices: marketData.prices,
|
|
4494
|
+
balances: Object.fromEntries(
|
|
4495
|
+
Object.entries(marketData.balances)
|
|
4496
|
+
.filter(([, v]) => v > 0n)
|
|
4497
|
+
.map(([k, v]) => [k, v.toString()])
|
|
4498
|
+
),
|
|
4499
|
+
portfolioValue: marketData.portfolioValue,
|
|
4500
|
+
})},
|
|
4092
4501
|
]);
|
|
4093
4502
|
|
|
4094
4503
|
// Parse and return signals
|
|
4095
4504
|
// IMPORTANT: Validate LLM output before using
|
|
4505
|
+
let content = response.content.trim();
|
|
4506
|
+
if (content.startsWith('\\\`\\\`\\\`')) {
|
|
4507
|
+
content = content.replace(/^\\\`\\\`\\\`(?:json)?\\n?/, '').replace(/\\n?\\\`\\\`\\\`$/, '').trim();
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4096
4510
|
try {
|
|
4097
|
-
const parsed = JSON.parse(
|
|
4511
|
+
const parsed = JSON.parse(content);
|
|
4098
4512
|
return parsed.signals || [];
|
|
4099
4513
|
} catch (e) {
|
|
4100
4514
|
console.error('Failed to parse LLM response:', e);
|
|
@@ -4103,6 +4517,9 @@ export const generateSignals: StrategyFunction = async (
|
|
|
4103
4517
|
};`
|
|
4104
4518
|
}
|
|
4105
4519
|
];
|
|
4520
|
+
function getStrategyTemplate(id) {
|
|
4521
|
+
return STRATEGY_TEMPLATES.find((t) => t.id === id);
|
|
4522
|
+
}
|
|
4106
4523
|
function getAllStrategyTemplates() {
|
|
4107
4524
|
return STRATEGY_TEMPLATES;
|
|
4108
4525
|
}
|
|
@@ -4250,6 +4667,8 @@ var AgentConfigSchema = import_zod.z.object({
|
|
|
4250
4667
|
perp: PerpConfigSchema,
|
|
4251
4668
|
// Prediction market configuration (Polymarket)
|
|
4252
4669
|
prediction: PredictionConfigSchema,
|
|
4670
|
+
// Custom strategy system prompt (overrides built-in template prompt)
|
|
4671
|
+
strategyPrompt: import_zod.z.string().optional(),
|
|
4253
4672
|
// Allowed tokens (addresses)
|
|
4254
4673
|
allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
|
|
4255
4674
|
});
|
|
@@ -4388,9 +4807,10 @@ var VAULT_ABI = [
|
|
|
4388
4807
|
outputs: [{ type: "uint256" }],
|
|
4389
4808
|
stateMutability: "view"
|
|
4390
4809
|
},
|
|
4810
|
+
// TOKEN → base asset (fee from output)
|
|
4391
4811
|
{
|
|
4392
4812
|
type: "function",
|
|
4393
|
-
name: "
|
|
4813
|
+
name: "executeSwap",
|
|
4394
4814
|
inputs: [
|
|
4395
4815
|
{ name: "tokenIn", type: "address" },
|
|
4396
4816
|
{ name: "tokenOut", type: "address" },
|
|
@@ -4398,6 +4818,44 @@ var VAULT_ABI = [
|
|
|
4398
4818
|
{ name: "minAmountOut", type: "uint256" },
|
|
4399
4819
|
{ name: "aggregator", type: "address" },
|
|
4400
4820
|
{ name: "swapData", type: "bytes" },
|
|
4821
|
+
{ name: "feeConvertAggregator", type: "address" },
|
|
4822
|
+
{ name: "feeSwapData", type: "bytes" },
|
|
4823
|
+
{ name: "deadline", type: "uint256" }
|
|
4824
|
+
],
|
|
4825
|
+
outputs: [{ type: "uint256" }],
|
|
4826
|
+
stateMutability: "nonpayable"
|
|
4827
|
+
},
|
|
4828
|
+
// Base asset → TOKEN (fee from input)
|
|
4829
|
+
{
|
|
4830
|
+
type: "function",
|
|
4831
|
+
name: "executeSwapSimple",
|
|
4832
|
+
inputs: [
|
|
4833
|
+
{ name: "tokenIn", type: "address" },
|
|
4834
|
+
{ name: "tokenOut", type: "address" },
|
|
4835
|
+
{ name: "amountIn", type: "uint256" },
|
|
4836
|
+
{ name: "minAmountOut", type: "uint256" },
|
|
4837
|
+
{ name: "target", type: "address" },
|
|
4838
|
+
{ name: "data", type: "bytes" },
|
|
4839
|
+
{ name: "feeConvertAggregator", type: "address" },
|
|
4840
|
+
{ name: "feeSwapData", type: "bytes" },
|
|
4841
|
+
{ name: "deadline", type: "uint256" }
|
|
4842
|
+
],
|
|
4843
|
+
outputs: [{ type: "uint256" }],
|
|
4844
|
+
stateMutability: "nonpayable"
|
|
4845
|
+
},
|
|
4846
|
+
// TOKEN → TOKEN (multi-hop via USDC, fee at midpoint)
|
|
4847
|
+
{
|
|
4848
|
+
type: "function",
|
|
4849
|
+
name: "executeSwapMultiHop",
|
|
4850
|
+
inputs: [
|
|
4851
|
+
{ name: "tokenIn", type: "address" },
|
|
4852
|
+
{ name: "tokenOut", type: "address" },
|
|
4853
|
+
{ name: "amountIn", type: "uint256" },
|
|
4854
|
+
{ name: "minAmountOut", type: "uint256" },
|
|
4855
|
+
{ name: "aggregator1", type: "address" },
|
|
4856
|
+
{ name: "swapData1", type: "bytes" },
|
|
4857
|
+
{ name: "aggregator2", type: "address" },
|
|
4858
|
+
{ name: "swapData2", type: "bytes" },
|
|
4401
4859
|
{ name: "deadline", type: "uint256" }
|
|
4402
4860
|
],
|
|
4403
4861
|
outputs: [{ type: "uint256" }],
|
|
@@ -4663,8 +5121,9 @@ var VaultManager = class {
|
|
|
4663
5121
|
}
|
|
4664
5122
|
}
|
|
4665
5123
|
/**
|
|
4666
|
-
* Execute a trade through the vault
|
|
4667
|
-
*
|
|
5124
|
+
* Execute a trade through the vault using API-provided vaultParams.
|
|
5125
|
+
* Dispatches to the correct vault function based on swapType.
|
|
5126
|
+
* Returns null if vault trading shouldn't be used (no vault, policy disabled).
|
|
4668
5127
|
*/
|
|
4669
5128
|
async executeVaultTrade(params) {
|
|
4670
5129
|
if (!this.preferVaultTrading) {
|
|
@@ -4675,23 +5134,71 @@ var VaultManager = class {
|
|
|
4675
5134
|
return null;
|
|
4676
5135
|
}
|
|
4677
5136
|
const deadline = params.deadline || BigInt(Math.floor(Date.now() / 1e3) + 3600);
|
|
5137
|
+
const { vaultParams } = params;
|
|
4678
5138
|
try {
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
5139
|
+
let hash;
|
|
5140
|
+
switch (vaultParams.swapType) {
|
|
5141
|
+
case "swap":
|
|
5142
|
+
hash = await this.walletClient.writeContract({
|
|
5143
|
+
address: vaultAddress,
|
|
5144
|
+
abi: VAULT_ABI,
|
|
5145
|
+
functionName: "executeSwap",
|
|
5146
|
+
args: [
|
|
5147
|
+
params.tokenIn,
|
|
5148
|
+
params.tokenOut,
|
|
5149
|
+
params.amountIn,
|
|
5150
|
+
params.minAmountOut,
|
|
5151
|
+
vaultParams.aggregator,
|
|
5152
|
+
vaultParams.swapData,
|
|
5153
|
+
vaultParams.feeConvertAggregator,
|
|
5154
|
+
vaultParams.feeSwapData,
|
|
5155
|
+
deadline
|
|
5156
|
+
],
|
|
5157
|
+
chain: import_chains2.base,
|
|
5158
|
+
account: this.account
|
|
5159
|
+
});
|
|
5160
|
+
break;
|
|
5161
|
+
case "simple":
|
|
5162
|
+
hash = await this.walletClient.writeContract({
|
|
5163
|
+
address: vaultAddress,
|
|
5164
|
+
abi: VAULT_ABI,
|
|
5165
|
+
functionName: "executeSwapSimple",
|
|
5166
|
+
args: [
|
|
5167
|
+
params.tokenIn,
|
|
5168
|
+
params.tokenOut,
|
|
5169
|
+
params.amountIn,
|
|
5170
|
+
params.minAmountOut,
|
|
5171
|
+
vaultParams.target,
|
|
5172
|
+
vaultParams.data,
|
|
5173
|
+
vaultParams.feeConvertAggregator,
|
|
5174
|
+
vaultParams.feeSwapData,
|
|
5175
|
+
deadline
|
|
5176
|
+
],
|
|
5177
|
+
chain: import_chains2.base,
|
|
5178
|
+
account: this.account
|
|
5179
|
+
});
|
|
5180
|
+
break;
|
|
5181
|
+
case "multihop":
|
|
5182
|
+
hash = await this.walletClient.writeContract({
|
|
5183
|
+
address: vaultAddress,
|
|
5184
|
+
abi: VAULT_ABI,
|
|
5185
|
+
functionName: "executeSwapMultiHop",
|
|
5186
|
+
args: [
|
|
5187
|
+
params.tokenIn,
|
|
5188
|
+
params.tokenOut,
|
|
5189
|
+
params.amountIn,
|
|
5190
|
+
params.minAmountOut,
|
|
5191
|
+
vaultParams.aggregator1,
|
|
5192
|
+
vaultParams.swapData1,
|
|
5193
|
+
vaultParams.aggregator2,
|
|
5194
|
+
vaultParams.swapData2,
|
|
5195
|
+
deadline
|
|
5196
|
+
],
|
|
5197
|
+
chain: import_chains2.base,
|
|
5198
|
+
account: this.account
|
|
5199
|
+
});
|
|
5200
|
+
break;
|
|
5201
|
+
}
|
|
4695
5202
|
return { usedVault: true, txHash: hash };
|
|
4696
5203
|
} catch (error) {
|
|
4697
5204
|
return {
|
|
@@ -7949,7 +8456,7 @@ function loadSecureEnv(basePath, passphrase) {
|
|
|
7949
8456
|
}
|
|
7950
8457
|
|
|
7951
8458
|
// src/index.ts
|
|
7952
|
-
var AGENT_VERSION = "0.1.
|
|
8459
|
+
var AGENT_VERSION = "0.1.48";
|
|
7953
8460
|
|
|
7954
8461
|
// src/relay.ts
|
|
7955
8462
|
var RelayClient = class {
|
|
@@ -8410,10 +8917,16 @@ var AgentRuntime = class {
|
|
|
8410
8917
|
vaultConfig
|
|
8411
8918
|
});
|
|
8412
8919
|
console.log(`Vault policy: ${vaultConfig.policy}`);
|
|
8920
|
+
if (this.executor) {
|
|
8921
|
+
this.executor.setVaultManager(this.vaultManager);
|
|
8922
|
+
}
|
|
8413
8923
|
const status = await this.vaultManager.getVaultStatus();
|
|
8414
8924
|
if (status.hasVault) {
|
|
8415
8925
|
console.log(`Vault exists: ${status.vaultAddress}`);
|
|
8416
8926
|
console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
|
|
8927
|
+
if (vaultConfig.preferVaultTrading) {
|
|
8928
|
+
console.log("Vault trading enabled \u2014 trades will execute from vault");
|
|
8929
|
+
}
|
|
8417
8930
|
} else {
|
|
8418
8931
|
console.log("No vault exists for this agent");
|
|
8419
8932
|
if (vaultConfig.policy === "manual") {
|
|
@@ -9834,8 +10347,9 @@ This exit has been recorded in your trade history and counts toward your total P
|
|
|
9834
10347
|
this.paperPortfolio.save();
|
|
9835
10348
|
} else {
|
|
9836
10349
|
const vaultStatus = await this.vaultManager?.getVaultStatus();
|
|
9837
|
-
if (vaultStatus?.hasVault
|
|
9838
|
-
|
|
10350
|
+
if (vaultStatus?.hasVault) {
|
|
10351
|
+
const via = this.vaultManager?.preferVaultTrading ? "vault" : "wallet";
|
|
10352
|
+
console.log(`Vault active: ${vaultStatus.vaultAddress} (trades execute from ${via})`);
|
|
9839
10353
|
}
|
|
9840
10354
|
const results = await this.executor.executeAll(filteredSignals);
|
|
9841
10355
|
let totalFeesUSD = 0;
|
|
@@ -10659,6 +11173,95 @@ ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
|
|
|
10659
11173
|
(0, import_dotenv2.config)({ path: envPath, override: true });
|
|
10660
11174
|
}
|
|
10661
11175
|
}
|
|
11176
|
+
program.command("init").description("Scaffold a new agent project with strategy template and config").option("-t, --template <template>", "Strategy template (momentum, value, arbitrage, custom)", "momentum").option("--force", "Overwrite existing files without prompting").action(async (options) => {
|
|
11177
|
+
try {
|
|
11178
|
+
const templateId = options.template;
|
|
11179
|
+
const template = getStrategyTemplate(templateId);
|
|
11180
|
+
if (!template) {
|
|
11181
|
+
const templates = getAllStrategyTemplates();
|
|
11182
|
+
console.error(`Unknown template: ${templateId}`);
|
|
11183
|
+
console.error(`Available templates: ${templates.map((t) => t.id).join(", ")}`);
|
|
11184
|
+
process.exit(1);
|
|
11185
|
+
}
|
|
11186
|
+
console.log("");
|
|
11187
|
+
console.log("=".repeat(50));
|
|
11188
|
+
console.log(" EXAGENT INIT");
|
|
11189
|
+
console.log("=".repeat(50));
|
|
11190
|
+
console.log("");
|
|
11191
|
+
console.log(` Template: ${template.name}`);
|
|
11192
|
+
console.log(` Risk: ${template.riskLevel}`);
|
|
11193
|
+
console.log("");
|
|
11194
|
+
const cwd = process.cwd();
|
|
11195
|
+
const strategyPath = path7.join(cwd, "strategy.ts");
|
|
11196
|
+
const configPath = path7.join(cwd, "agent-config.json");
|
|
11197
|
+
const existingFiles = [];
|
|
11198
|
+
if (fs6.existsSync(strategyPath)) existingFiles.push("strategy.ts");
|
|
11199
|
+
if (fs6.existsSync(configPath)) existingFiles.push("agent-config.json");
|
|
11200
|
+
if (existingFiles.length > 0 && !options.force) {
|
|
11201
|
+
console.log(` Files already exist: ${existingFiles.join(", ")}`);
|
|
11202
|
+
const answer = await prompt(" Overwrite? (y/n): ");
|
|
11203
|
+
if (answer.toLowerCase() !== "y") {
|
|
11204
|
+
console.log(" Aborted.");
|
|
11205
|
+
process.exit(0);
|
|
11206
|
+
}
|
|
11207
|
+
console.log("");
|
|
11208
|
+
}
|
|
11209
|
+
fs6.writeFileSync(strategyPath, template.exampleCode.trim() + "\n");
|
|
11210
|
+
console.log(" Created: strategy.ts");
|
|
11211
|
+
const defaultConfig = {
|
|
11212
|
+
agentId: 0,
|
|
11213
|
+
name: "my-agent",
|
|
11214
|
+
network: "mainnet",
|
|
11215
|
+
llm: {
|
|
11216
|
+
provider: "openai",
|
|
11217
|
+
model: "gpt-4o",
|
|
11218
|
+
temperature: 0.7,
|
|
11219
|
+
maxTokens: 4096
|
|
11220
|
+
},
|
|
11221
|
+
riskUniverse: "established",
|
|
11222
|
+
trading: {
|
|
11223
|
+
timeHorizon: "swing",
|
|
11224
|
+
maxPositionSizeBps: 1e3,
|
|
11225
|
+
maxDailyLossBps: 500,
|
|
11226
|
+
maxConcurrentPositions: 5,
|
|
11227
|
+
tradingIntervalMs: 6e4,
|
|
11228
|
+
maxSlippageBps: 100,
|
|
11229
|
+
minTradeValueUSD: 1
|
|
11230
|
+
},
|
|
11231
|
+
wallet: {
|
|
11232
|
+
setup: "generate"
|
|
11233
|
+
},
|
|
11234
|
+
relay: {
|
|
11235
|
+
enabled: true,
|
|
11236
|
+
apiUrl: "https://exagent-api.onrender.com"
|
|
11237
|
+
}
|
|
11238
|
+
};
|
|
11239
|
+
if (!fs6.existsSync(configPath) || options.force || existingFiles.includes("agent-config.json")) {
|
|
11240
|
+
fs6.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
11241
|
+
console.log(" Created: agent-config.json");
|
|
11242
|
+
}
|
|
11243
|
+
console.log("");
|
|
11244
|
+
console.log("=".repeat(50));
|
|
11245
|
+
console.log(" NEXT STEPS");
|
|
11246
|
+
console.log("=".repeat(50));
|
|
11247
|
+
console.log("");
|
|
11248
|
+
console.log(" 1. Register your agent at https://exagent.io/deploy");
|
|
11249
|
+
console.log(" 2. Copy your agent ID into agent-config.json");
|
|
11250
|
+
console.log(" 3. Update agent-config.json with your name and LLM provider");
|
|
11251
|
+
console.log(" 4. Run: npx @exagent/agent paper");
|
|
11252
|
+
console.log(" (paper trade first to validate your strategy)");
|
|
11253
|
+
console.log("");
|
|
11254
|
+
console.log(" 5. When ready for real trading:");
|
|
11255
|
+
console.log(" npx @exagent/agent run");
|
|
11256
|
+
console.log("");
|
|
11257
|
+
console.log("=".repeat(50));
|
|
11258
|
+
console.log("");
|
|
11259
|
+
process.exit(0);
|
|
11260
|
+
} catch (error) {
|
|
11261
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
11262
|
+
process.exit(1);
|
|
11263
|
+
}
|
|
11264
|
+
});
|
|
10662
11265
|
program.command("run").description("Start the trading agent").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").action(async (options) => {
|
|
10663
11266
|
try {
|
|
10664
11267
|
await checkFirstRunSetup(options.config);
|