@exagent/agent 0.1.47 → 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/index.js CHANGED
@@ -560,6 +560,7 @@ var TOKEN_TO_COINGECKO = {
560
560
  };
561
561
  var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai", "tether"]);
562
562
  var PRICE_STALENESS_MS = 6e4;
563
+ var HISTORY_STALENESS_MS = 60 * 6e4;
563
564
  var MarketDataService = class {
564
565
  rpcUrl;
565
566
  client;
@@ -572,6 +573,10 @@ var MarketDataService = class {
572
573
  cachedVolume24h = {};
573
574
  /** Cached price change data */
574
575
  cachedPriceChange24h = {};
576
+ /** Cached 24h hourly price history per token address */
577
+ cachedPriceHistory = {};
578
+ /** Timestamp of last successful price history fetch */
579
+ lastHistoryFetchAt = 0;
575
580
  constructor(rpcUrl, store) {
576
581
  this.rpcUrl = rpcUrl;
577
582
  this.client = (0, import_viem2.createPublicClient)({
@@ -592,11 +597,10 @@ var MarketDataService = class {
592
597
  const prices = await this.fetchPrices(tokenAddresses);
593
598
  const balances = await this.fetchBalances(walletAddress, tokenAddresses);
594
599
  const portfolioValue = this.calculatePortfolioValue(balances, prices);
595
- let gasPrice;
596
- try {
597
- gasPrice = await this.client.getGasPrice();
598
- } catch {
599
- }
600
+ const [priceHistory, gasPrice] = await Promise.all([
601
+ this.fetchPriceHistory(tokenAddresses).catch(() => void 0),
602
+ this.client.getGasPrice().catch(() => void 0)
603
+ ]);
600
604
  return {
601
605
  timestamp: Date.now(),
602
606
  prices,
@@ -607,7 +611,8 @@ var MarketDataService = class {
607
611
  gasPrice,
608
612
  network: {
609
613
  chainId: this.client.chain?.id ?? 8453
610
- }
614
+ },
615
+ priceHistory: priceHistory && Object.keys(priceHistory).length > 0 ? priceHistory : void 0
611
616
  };
612
617
  }
613
618
  /**
@@ -780,6 +785,58 @@ var MarketDataService = class {
780
785
  }
781
786
  return prices;
782
787
  }
788
+ /**
789
+ * Fetch 24h hourly price history from CoinGecko.
790
+ * Returns cached data if still fresh (< 60 min old).
791
+ * Only fetches for tokens with known CoinGecko IDs.
792
+ */
793
+ async fetchPriceHistory(tokenAddresses) {
794
+ if (Date.now() - this.lastHistoryFetchAt < HISTORY_STALENESS_MS && Object.keys(this.cachedPriceHistory).length > 0) {
795
+ return { ...this.cachedPriceHistory };
796
+ }
797
+ const history = {};
798
+ const idToAddrs = {};
799
+ for (const addr of tokenAddresses) {
800
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
801
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
802
+ if (!idToAddrs[cgId]) idToAddrs[cgId] = [];
803
+ idToAddrs[cgId].push(addr.toLowerCase());
804
+ }
805
+ }
806
+ const ids = Object.keys(idToAddrs);
807
+ for (let i = 0; i < ids.length; i++) {
808
+ const cgId = ids[i];
809
+ try {
810
+ const resp = await fetch(
811
+ `https://api.coingecko.com/api/v3/coins/${cgId}/market_chart?vs_currency=usd&days=1`,
812
+ { signal: AbortSignal.timeout(5e3) }
813
+ );
814
+ if (!resp.ok) {
815
+ if (resp.status === 429) {
816
+ console.warn("CoinGecko rate limit hit during price history fetch \u2014 using partial data");
817
+ break;
818
+ }
819
+ continue;
820
+ }
821
+ const data = await resp.json();
822
+ if (data.prices && Array.isArray(data.prices)) {
823
+ const candles = data.prices.map(([ts, price]) => ({ timestamp: ts, price }));
824
+ for (const addr of idToAddrs[cgId]) {
825
+ history[addr] = candles;
826
+ }
827
+ }
828
+ } catch {
829
+ }
830
+ if (i < ids.length - 1) {
831
+ await new Promise((r) => setTimeout(r, 2500));
832
+ }
833
+ }
834
+ if (Object.keys(history).length > 0) {
835
+ this.cachedPriceHistory = { ...this.cachedPriceHistory, ...history };
836
+ this.lastHistoryFetchAt = Date.now();
837
+ }
838
+ return { ...this.cachedPriceHistory };
839
+ }
783
840
  /**
784
841
  * Fetch real on-chain balances: native ETH + ERC-20 tokens.
785
842
  * Uses Multicall3 to batch all balanceOf calls into a single RPC request.
@@ -1897,7 +1954,7 @@ var STRATEGY_TEMPLATES = [
1897
1954
  {
1898
1955
  id: "momentum",
1899
1956
  name: "Momentum Trader",
1900
- description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum.",
1957
+ description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum, sells when momentum fades.",
1901
1958
  riskLevel: "medium",
1902
1959
  riskWarnings: [
1903
1960
  "Momentum strategies can suffer significant losses during trend reversals",
@@ -1905,60 +1962,248 @@ var STRATEGY_TEMPLATES = [
1905
1962
  "Past performance does not guarantee future results",
1906
1963
  "This strategy may underperform in sideways markets"
1907
1964
  ],
1908
- systemPrompt: `You are an AI trading analyst specializing in momentum trading strategies.
1965
+ systemPrompt: `You are an AI trading analyst specializing in momentum trading on Base network.
1909
1966
 
1910
- Your role is to analyze market data and identify momentum-based trading opportunities.
1967
+ Analyze the provided market data and identify momentum-based trading opportunities.
1911
1968
 
1912
- IMPORTANT CONSTRAINTS:
1969
+ RULES:
1913
1970
  - Only recommend trades when there is clear momentum evidence
1914
- - Always consider risk/reward ratios
1915
- - Never recommend more than the configured position size limits
1916
- - Be conservative with confidence scores
1971
+ - Use priceTrends (1h, 4h, 12h, 24h changes) as your primary momentum signal
1972
+ - Fall back to priceChange24h if priceTrends is not available
1973
+ - Be conservative with confidence \u2014 only use > 0.6 for strong signals
1974
+ - Never recommend buying a token you already hold significant amounts of
1975
+ - Always consider selling positions that have lost momentum
1976
+ - Return "hold" signals when uncertain \u2014 doing nothing is often the best trade
1917
1977
 
1918
- When analyzing data, look for:
1919
- 1. Price trends (higher highs, higher lows for uptrends)
1920
- 2. Volume confirmation (increasing volume on moves)
1921
- 3. Relative strength vs market benchmarks
1978
+ ANALYZE:
1979
+ 1. Multi-timeframe momentum: use priceTrends (1h, 4h, 12h, 24h) to confirm trend direction
1980
+ 2. Strong momentum = positive across all timeframes. Divergence (e.g., 1h negative, 12h positive) = weakening
1981
+ 3. Volume confirmation: prefer tokens with > $10K daily volume
1982
+ 4. Portfolio concentration: avoid putting > 20% in any single token
1983
+ 5. Exit signals: sell when short-term trend (1h, 4h) turns negative on a held position
1922
1984
 
1923
- Respond with JSON in this format:
1985
+ Respond with ONLY valid JSON (no markdown, no code fences):
1924
1986
  {
1925
- "analysis": "Brief market analysis",
1987
+ "analysis": "Brief market analysis (1-2 sentences)",
1926
1988
  "signals": [
1927
1989
  {
1928
1990
  "action": "buy" | "sell" | "hold",
1929
- "tokenIn": "0x...",
1930
- "tokenOut": "0x...",
1931
- "percentage": 0-100,
1932
- "confidence": 0-1,
1933
- "reasoning": "Why this trade"
1991
+ "token": "0x... (token address to buy/sell)",
1992
+ "percentage": 5-25,
1993
+ "confidence": 0.0-1.0,
1994
+ "reasoning": "Why this trade (1 sentence)"
1934
1995
  }
1935
1996
  ]
1936
- }`,
1937
- exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
1997
+ }
1998
+
1999
+ If no good opportunities exist, return: { "analysis": "No clear momentum signals", "signals": [] }`,
2000
+ exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
2001
+
2002
+ // Well-known token addresses on Base
2003
+ const WETH = '0x4200000000000000000000000000000000000006';
2004
+ const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
2005
+
2006
+ // Token decimals lookup (extend as needed)
2007
+ const DECIMALS: Record<string, number> = {
2008
+ [WETH]: 18,
2009
+ [USDC]: 6,
2010
+ };
2011
+
2012
+ /**
2013
+ * Format balances for LLM consumption (human-readable amounts)
2014
+ */
2015
+ function formatBalances(balances: Record<string, bigint>, prices: Record<string, number>): Record<string, { amount: string; usdValue: string }> {
2016
+ const result: Record<string, { amount: string; usdValue: string }> = {};
2017
+ for (const [token, balance] of Object.entries(balances)) {
2018
+ if (balance === 0n) continue;
2019
+ const decimals = DECIMALS[token.toLowerCase()] ?? 18;
2020
+ const amount = Number(balance) / (10 ** decimals);
2021
+ const price = prices[token.toLowerCase()] ?? 0;
2022
+ result[token] = {
2023
+ amount: amount.toFixed(decimals <= 8 ? decimals : 6),
2024
+ usdValue: (amount * price).toFixed(2),
2025
+ };
2026
+ }
2027
+ return result;
2028
+ }
2029
+
2030
+ /**
2031
+ * Convert LLM percentage signal to a TradeSignal with bigint amountIn
2032
+ *
2033
+ * The LLM says "buy token X with 10% of my ETH" \u2014 this function converts
2034
+ * that percentage into the actual wei amount to trade.
2035
+ */
2036
+ function toTradeSignal(
2037
+ signal: { action: string; token: string; percentage: number; confidence: number; reasoning?: string },
2038
+ balances: Record<string, bigint>,
2039
+ ): TradeSignal | null {
2040
+ const { action, token, percentage, confidence, reasoning } = signal;
2041
+
2042
+ // Skip hold signals and low confidence
2043
+ if (action === 'hold' || confidence < 0.5) return null;
1938
2044
 
2045
+ // Clamp percentage to 1-50 range for safety
2046
+ const pct = Math.max(1, Math.min(50, percentage));
2047
+
2048
+ if (action === 'buy') {
2049
+ // Buying: spend ETH (or USDC) to acquire the target token
2050
+ // Use ETH balance as the source
2051
+ const ethBalance = balances[WETH] ?? 0n;
2052
+ if (ethBalance === 0n) return null;
2053
+ const amountIn = (ethBalance * BigInt(Math.round(pct * 100))) / 10000n;
2054
+ if (amountIn === 0n) return null;
2055
+ return { action: 'buy', tokenIn: WETH, tokenOut: token, amountIn, confidence, reasoning };
2056
+ }
2057
+
2058
+ if (action === 'sell') {
2059
+ // Selling: sell the target token for ETH
2060
+ const tokenBalance = balances[token.toLowerCase()] ?? balances[token] ?? 0n;
2061
+ if (tokenBalance === 0n) return null;
2062
+ const amountIn = (tokenBalance * BigInt(Math.round(pct * 100))) / 10000n;
2063
+ if (amountIn === 0n) return null;
2064
+ return { action: 'sell', tokenIn: token, tokenOut: WETH, amountIn, confidence, reasoning };
2065
+ }
2066
+
2067
+ return null;
2068
+ }
2069
+
2070
+ /**
2071
+ * Momentum Trading Strategy
2072
+ *
2073
+ * Sends market data to the LLM, parses the response, and converts
2074
+ * percentage-based signals into executable TradeSignal objects.
2075
+ */
1939
2076
  export const generateSignals: StrategyFunction = async (
1940
2077
  marketData: MarketData,
1941
2078
  llm: LLMAdapter,
1942
- config: AgentConfig
2079
+ config: AgentConfig,
2080
+ context?: StrategyContext,
1943
2081
  ): Promise<TradeSignal[]> => {
2082
+ // Summarize price history into compact trend data for the LLM
2083
+ // Instead of sending 24 raw candles per token, compute useful signals:
2084
+ // - 1h, 4h, 12h, 24h price changes (momentum at different timeframes)
2085
+ const priceTrends: Record<string, { '1h': string; '4h': string; '12h': string; '24h': string }> = {};
2086
+ if (marketData.priceHistory) {
2087
+ for (const [addr, candles] of Object.entries(marketData.priceHistory)) {
2088
+ if (candles.length < 2) continue;
2089
+ const latest = candles[candles.length - 1].price;
2090
+ const getChangeAt = (hoursAgo: number): string => {
2091
+ const target = Date.now() - hoursAgo * 3600_000;
2092
+ // Find the candle closest to the target time
2093
+ let closest = candles[0];
2094
+ for (const c of candles) {
2095
+ if (Math.abs(c.timestamp - target) < Math.abs(closest.timestamp - target)) closest = c;
2096
+ }
2097
+ if (closest.price === 0) return '0.0%';
2098
+ const pct = ((latest - closest.price) / closest.price) * 100;
2099
+ return (pct >= 0 ? '+' : '') + pct.toFixed(1) + '%';
2100
+ };
2101
+ priceTrends[addr] = {
2102
+ '1h': getChangeAt(1),
2103
+ '4h': getChangeAt(4),
2104
+ '12h': getChangeAt(12),
2105
+ '24h': getChangeAt(24),
2106
+ };
2107
+ }
2108
+ }
2109
+
2110
+ // Build the data payload for the LLM
2111
+ const payload: Record<string, unknown> = {
2112
+ prices: marketData.prices,
2113
+ balances: formatBalances(marketData.balances, marketData.prices),
2114
+ portfolioValueUSD: marketData.portfolioValue.toFixed(2),
2115
+ priceChange24h: marketData.priceChange24h ?? {},
2116
+ volume24h: marketData.volume24h ?? {},
2117
+ recentTrades: (context?.tradeHistory ?? []).slice(0, 5).map(t => ({
2118
+ action: t.action,
2119
+ token: t.action === 'buy' ? t.tokenOut : t.tokenIn,
2120
+ reasoning: t.reasoning,
2121
+ timestamp: new Date(t.timestamp).toISOString(),
2122
+ })),
2123
+ };
2124
+
2125
+ // Include price trends if available (more useful than raw candles for LLM)
2126
+ if (Object.keys(priceTrends).length > 0) {
2127
+ payload.priceTrends = priceTrends;
2128
+ }
2129
+
2130
+ // Call the LLM
1944
2131
  const response = await llm.chat([
1945
- { role: 'system', content: MOMENTUM_SYSTEM_PROMPT },
1946
- { role: 'user', content: JSON.stringify({
1947
- prices: marketData.prices,
1948
- balances: formatBalances(marketData.balances),
1949
- portfolioValue: marketData.portfolioValue,
1950
- })}
2132
+ { role: 'system', content: config.strategyPrompt ?? MOMENTUM_SYSTEM_PROMPT },
2133
+ { role: 'user', content: JSON.stringify(payload) },
1951
2134
  ]);
1952
2135
 
1953
- // Parse LLM response and convert to TradeSignals
1954
- const parsed = JSON.parse(response.content);
1955
- return parsed.signals.map(convertToTradeSignal);
1956
- };`
2136
+ // Parse the response \u2014 handle markdown code fences if the LLM wraps its output
2137
+ let content = response.content.trim();
2138
+ if (content.startsWith('\`\`\`')) {
2139
+ content = content.replace(/^\`\`\`(?:json)?\\n?/, '').replace(/\\n?\`\`\`$/, '').trim();
2140
+ }
2141
+
2142
+ let parsed: { signals?: Array<{ action: string; token: string; percentage: number; confidence: number; reasoning?: string }> };
2143
+ try {
2144
+ parsed = JSON.parse(content);
2145
+ } catch (e) {
2146
+ console.error('[strategy] Failed to parse LLM response:', (e as Error).message);
2147
+ console.error('[strategy] Raw response:', content.slice(0, 200));
2148
+ return []; // Safe fallback: no trades
2149
+ }
2150
+
2151
+ if (!parsed.signals || !Array.isArray(parsed.signals)) {
2152
+ return [];
2153
+ }
2154
+
2155
+ // Convert each signal to a TradeSignal with proper bigint amountIn
2156
+ const signals: TradeSignal[] = [];
2157
+ for (const raw of parsed.signals) {
2158
+ const signal = toTradeSignal(raw, marketData.balances);
2159
+ if (signal) signals.push(signal);
2160
+ }
2161
+
2162
+ return signals;
2163
+ };
2164
+
2165
+ // Default system prompt \u2014 used when config.strategyPrompt is not set
2166
+ const MOMENTUM_SYSTEM_PROMPT = \`You are an AI trading analyst specializing in momentum trading on Base network.
2167
+
2168
+ Analyze the provided market data and identify momentum-based trading opportunities.
2169
+
2170
+ RULES:
2171
+ - Only recommend trades when there is clear momentum evidence
2172
+ - Use priceTrends (1h, 4h, 12h, 24h changes) as your primary momentum signal
2173
+ - Fall back to priceChange24h if priceTrends is not available
2174
+ - Be conservative with confidence \u2014 only use > 0.6 for strong signals
2175
+ - Never recommend buying a token you already hold significant amounts of
2176
+ - Always consider selling positions that have lost momentum
2177
+ - Return "hold" signals when uncertain \u2014 doing nothing is often the best trade
2178
+
2179
+ ANALYZE:
2180
+ 1. Multi-timeframe momentum: use priceTrends (1h, 4h, 12h, 24h) to confirm trend direction
2181
+ 2. Strong momentum = positive across all timeframes. Divergence (e.g., 1h negative, 12h positive) = weakening
2182
+ 3. Volume confirmation: prefer tokens with > $10K daily volume
2183
+ 4. Portfolio concentration: avoid putting > 20% in any single token
2184
+ 5. Exit signals: sell when short-term trend (1h, 4h) turns negative on a held position
2185
+
2186
+ Respond with ONLY valid JSON (no markdown, no code fences):
2187
+ {
2188
+ "analysis": "Brief market analysis (1-2 sentences)",
2189
+ "signals": [
2190
+ {
2191
+ "action": "buy" | "sell" | "hold",
2192
+ "token": "0x... (token address to buy/sell)",
2193
+ "percentage": 5-25,
2194
+ "confidence": 0.0-1.0,
2195
+ "reasoning": "Why this trade (1 sentence)"
2196
+ }
2197
+ ]
2198
+ }
2199
+
2200
+ If no good opportunities exist, return: { "analysis": "No clear momentum signals", "signals": [] }\`;
2201
+ `
1957
2202
  },
1958
2203
  {
1959
2204
  id: "value",
1960
2205
  name: "Value Investor",
1961
- description: "Looks for undervalued assets based on fundamentals. Takes long-term positions.",
2206
+ description: "Looks for undervalued assets based on fundamentals. Takes long-term positions with lower turnover.",
1962
2207
  riskLevel: "low",
1963
2208
  riskWarnings: [
1964
2209
  "Value traps can result in prolonged losses",
@@ -1966,47 +2211,135 @@ export const generateSignals: StrategyFunction = async (
1966
2211
  "Fundamental analysis may not apply well to all crypto assets",
1967
2212
  "Market sentiment can override fundamentals for long periods"
1968
2213
  ],
1969
- systemPrompt: `You are an AI trading analyst specializing in value investing.
2214
+ systemPrompt: `You are an AI trading analyst specializing in value investing on Base network.
1970
2215
 
1971
2216
  Your role is to identify undervalued assets with strong fundamentals.
1972
2217
 
1973
- IMPORTANT CONSTRAINTS:
2218
+ RULES:
1974
2219
  - Focus on long-term value, not short-term price movements
1975
2220
  - Only recommend assets with clear value propositions
1976
2221
  - Consider protocol revenue, TVL, active users, developer activity
1977
- - Be very selective - quality over quantity
1978
-
1979
- When analyzing, consider:
1980
- 1. Protocol fundamentals (revenue, TVL, user growth)
1981
- 2. Token economics (supply schedule, utility)
1982
- 3. Competitive positioning
1983
- 4. Valuation relative to peers
2222
+ - Be very selective \u2014 quality over quantity
2223
+ - Prefer established tokens (AERO, WELL, MORPHO, COMP, CRV) over memecoins
2224
+ - Hold positions for days/weeks, not hours
1984
2225
 
1985
- Respond with JSON in this format:
2226
+ Respond with ONLY valid JSON:
1986
2227
  {
1987
2228
  "analysis": "Brief fundamental analysis",
1988
2229
  "signals": [
1989
2230
  {
1990
2231
  "action": "buy" | "sell" | "hold",
1991
- "tokenIn": "0x...",
1992
- "tokenOut": "0x...",
1993
- "percentage": 0-100,
1994
- "confidence": 0-1,
2232
+ "token": "0x... (token address)",
2233
+ "percentage": 5-20,
2234
+ "confidence": 0.0-1.0,
1995
2235
  "reasoning": "Fundamental thesis"
1996
2236
  }
1997
2237
  ]
1998
2238
  }`,
1999
- exampleCode: `import { StrategyFunction } from '@exagent/agent';
2239
+ exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
2240
+
2241
+ const WETH = '0x4200000000000000000000000000000000000006';
2242
+ const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
2243
+ const DECIMALS: Record<string, number> = { [WETH]: 18, [USDC]: 6 };
2244
+
2245
+ function formatBalances(balances: Record<string, bigint>, prices: Record<string, number>): Record<string, { amount: string; usdValue: string }> {
2246
+ const result: Record<string, { amount: string; usdValue: string }> = {};
2247
+ for (const [token, balance] of Object.entries(balances)) {
2248
+ if (balance === 0n) continue;
2249
+ const decimals = DECIMALS[token.toLowerCase()] ?? 18;
2250
+ const amount = Number(balance) / (10 ** decimals);
2251
+ const price = prices[token.toLowerCase()] ?? 0;
2252
+ result[token] = { amount: amount.toFixed(6), usdValue: (amount * price).toFixed(2) };
2253
+ }
2254
+ return result;
2255
+ }
2256
+
2257
+ function toTradeSignal(
2258
+ signal: { action: string; token: string; percentage: number; confidence: number; reasoning?: string },
2259
+ balances: Record<string, bigint>,
2260
+ ): TradeSignal | null {
2261
+ if (signal.action === 'hold' || signal.confidence < 0.5) return null;
2262
+ const pct = Math.max(1, Math.min(50, signal.percentage));
2263
+ if (signal.action === 'buy') {
2264
+ const ethBalance = balances[WETH] ?? 0n;
2265
+ if (ethBalance === 0n) return null;
2266
+ const amountIn = (ethBalance * BigInt(Math.round(pct * 100))) / 10000n;
2267
+ if (amountIn === 0n) return null;
2268
+ return { action: 'buy', tokenIn: WETH, tokenOut: signal.token, amountIn, confidence: signal.confidence, reasoning: signal.reasoning };
2269
+ }
2270
+ if (signal.action === 'sell') {
2271
+ const tokenBalance = balances[signal.token.toLowerCase()] ?? balances[signal.token] ?? 0n;
2272
+ if (tokenBalance === 0n) return null;
2273
+ const amountIn = (tokenBalance * BigInt(Math.round(pct * 100))) / 10000n;
2274
+ if (amountIn === 0n) return null;
2275
+ return { action: 'sell', tokenIn: signal.token, tokenOut: WETH, amountIn, confidence: signal.confidence, reasoning: signal.reasoning };
2276
+ }
2277
+ return null;
2278
+ }
2279
+
2280
+ export const generateSignals: StrategyFunction = async (
2281
+ marketData: MarketData,
2282
+ llm: LLMAdapter,
2283
+ config: AgentConfig,
2284
+ context?: StrategyContext,
2285
+ ): Promise<TradeSignal[]> => {
2286
+ const payload = {
2287
+ prices: marketData.prices,
2288
+ balances: formatBalances(marketData.balances, marketData.prices),
2289
+ portfolioValueUSD: marketData.portfolioValue.toFixed(2),
2290
+ priceChange24h: marketData.priceChange24h ?? {},
2291
+ positions: (context?.positions ?? []).map(p => ({
2292
+ token: p.tokenAddress,
2293
+ entryPrice: p.averageEntryPrice,
2294
+ currentAmount: p.currentAmount.toString(),
2295
+ })),
2296
+ };
2000
2297
 
2001
- export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
2002
- // Value strategy runs less frequently
2003
2298
  const response = await llm.chat([
2004
- { role: 'system', content: VALUE_SYSTEM_PROMPT },
2005
- { role: 'user', content: JSON.stringify(marketData) }
2299
+ { role: 'system', content: config.strategyPrompt ?? VALUE_SYSTEM_PROMPT },
2300
+ { role: 'user', content: JSON.stringify(payload) },
2006
2301
  ]);
2007
2302
 
2008
- return parseSignals(response.content);
2009
- };`
2303
+ let content = response.content.trim();
2304
+ if (content.startsWith('\\\`\\\`\\\`')) {
2305
+ content = content.replace(/^\\\`\\\`\\\`(?:json)?\\n?/, '').replace(/\\n?\\\`\\\`\\\`$/, '').trim();
2306
+ }
2307
+
2308
+ try {
2309
+ const parsed = JSON.parse(content);
2310
+ if (!parsed.signals || !Array.isArray(parsed.signals)) return [];
2311
+ return parsed.signals.map((s: any) => toTradeSignal(s, marketData.balances)).filter(Boolean) as TradeSignal[];
2312
+ } catch (e) {
2313
+ console.error('[strategy] Failed to parse LLM response:', (e as Error).message);
2314
+ return [];
2315
+ }
2316
+ };
2317
+
2318
+ const VALUE_SYSTEM_PROMPT = \`You are an AI trading analyst specializing in value investing on Base network.
2319
+
2320
+ Your role is to identify undervalued assets with strong fundamentals.
2321
+
2322
+ RULES:
2323
+ - Focus on long-term value, not short-term price movements
2324
+ - Only recommend assets with clear value propositions
2325
+ - Be very selective \u2014 quality over quantity
2326
+ - Prefer established tokens over memecoins
2327
+ - Hold positions for days/weeks, not hours
2328
+
2329
+ Respond with ONLY valid JSON:
2330
+ {
2331
+ "analysis": "Brief fundamental analysis",
2332
+ "signals": [
2333
+ {
2334
+ "action": "buy" | "sell" | "hold",
2335
+ "token": "0x... (token address)",
2336
+ "percentage": 5-20,
2337
+ "confidence": 0.0-1.0,
2338
+ "reasoning": "Fundamental thesis"
2339
+ }
2340
+ ]
2341
+ }\`;
2342
+ `
2010
2343
  },
2011
2344
  {
2012
2345
  id: "arbitrage",
@@ -2046,7 +2379,7 @@ Respond with JSON in this format:
2046
2379
  exampleCode: `// Note: Pure arbitrage requires specialized infrastructure
2047
2380
  // This template is for educational purposes
2048
2381
 
2049
- import { StrategyFunction } from '@exagent/agent';
2382
+ import type { StrategyFunction } from '@exagent/agent';
2050
2383
 
2051
2384
  export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
2052
2385
  // Arbitrage requires real-time price feeds from multiple sources
@@ -2078,18 +2411,21 @@ Respond with JSON:
2078
2411
  {
2079
2412
  "signals": []
2080
2413
  }`,
2081
- exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
2414
+ exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
2415
+
2416
+ const WETH = '0x4200000000000000000000000000000000000006';
2082
2417
 
2083
2418
  /**
2084
2419
  * Custom Strategy Template
2085
2420
  *
2086
2421
  * Customize this file with your own trading logic and prompts.
2087
- * Your prompts are YOUR intellectual property - we don't store them.
2422
+ * Your prompts are YOUR intellectual property \u2014 we don't store them.
2088
2423
  */
2089
2424
  export const generateSignals: StrategyFunction = async (
2090
2425
  marketData: MarketData,
2091
2426
  llm: LLMAdapter,
2092
- config: AgentConfig
2427
+ config: AgentConfig,
2428
+ context?: StrategyContext,
2093
2429
  ): Promise<TradeSignal[]> => {
2094
2430
  // Your custom system prompt (this is your secret sauce)
2095
2431
  const systemPrompt = \`
@@ -2099,13 +2435,26 @@ export const generateSignals: StrategyFunction = async (
2099
2435
  // Call the LLM with your prompt
2100
2436
  const response = await llm.chat([
2101
2437
  { role: 'system', content: systemPrompt },
2102
- { role: 'user', content: JSON.stringify(marketData) }
2438
+ { role: 'user', content: JSON.stringify({
2439
+ prices: marketData.prices,
2440
+ balances: Object.fromEntries(
2441
+ Object.entries(marketData.balances)
2442
+ .filter(([, v]) => v > 0n)
2443
+ .map(([k, v]) => [k, v.toString()])
2444
+ ),
2445
+ portfolioValue: marketData.portfolioValue,
2446
+ })},
2103
2447
  ]);
2104
2448
 
2105
2449
  // Parse and return signals
2106
2450
  // IMPORTANT: Validate LLM output before using
2451
+ let content = response.content.trim();
2452
+ if (content.startsWith('\\\`\\\`\\\`')) {
2453
+ content = content.replace(/^\\\`\\\`\\\`(?:json)?\\n?/, '').replace(/\\n?\\\`\\\`\\\`$/, '').trim();
2454
+ }
2455
+
2107
2456
  try {
2108
- const parsed = JSON.parse(response.content);
2457
+ const parsed = JSON.parse(content);
2109
2458
  return parsed.signals || [];
2110
2459
  } catch (e) {
2111
2460
  console.error('Failed to parse LLM response:', e);
@@ -2264,6 +2613,8 @@ var AgentConfigSchema = import_zod.z.object({
2264
2613
  perp: PerpConfigSchema,
2265
2614
  // Prediction market configuration (Polymarket)
2266
2615
  prediction: PredictionConfigSchema,
2616
+ // Custom strategy system prompt (overrides built-in template prompt)
2617
+ strategyPrompt: import_zod.z.string().optional(),
2267
2618
  // Allowed tokens (addresses)
2268
2619
  allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
2269
2620
  });
@@ -2468,6 +2819,10 @@ var TradeExecutor = class {
2468
2819
  * Validate a signal against config limits and token restrictions
2469
2820
  */
2470
2821
  validateSignal(signal) {
2822
+ if (signal.amountIn <= 0n) {
2823
+ console.warn(`Signal amountIn is ${signal.amountIn} \u2014 skipping (must be positive)`);
2824
+ return false;
2825
+ }
2471
2826
  if (signal.confidence < 0.5) {
2472
2827
  console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
2473
2828
  return false;
@@ -4133,6 +4488,33 @@ function formatSessionReport(result) {
4133
4488
  }
4134
4489
  lines.push("");
4135
4490
  }
4491
+ if (result.trades.length > 0) {
4492
+ lines.push(` ${thinDivider}`);
4493
+ lines.push(" TRADE LOG");
4494
+ lines.push(` ${thinDivider}`);
4495
+ lines.push("");
4496
+ const recentTrades = result.trades.slice(-50);
4497
+ for (const t of recentTrades) {
4498
+ const time = new Date(t.timestamp).toLocaleString(void 0, {
4499
+ month: "short",
4500
+ day: "2-digit",
4501
+ hour: "2-digit",
4502
+ minute: "2-digit"
4503
+ });
4504
+ const tag = t.action === "buy" ? "BUY " : "SELL";
4505
+ const tokenIn = t.tokenIn.slice(0, 6) + "\u2026";
4506
+ const tokenOut = t.tokenOut.slice(0, 6) + "\u2026";
4507
+ const value = `$${t.valueInUSD.toFixed(2)}`;
4508
+ const pnl = t.valueOutUSD - t.valueInUSD - t.feeUSD - t.gasUSD;
4509
+ const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}`;
4510
+ const reason = t.reasoning ? ` "${t.reasoning.slice(0, 60)}"` : "";
4511
+ lines.push(` ${time} ${tag} ${tokenIn}\u2192${tokenOut} ${value} ${pnlStr}${reason}`);
4512
+ }
4513
+ if (result.trades.length > 50) {
4514
+ lines.push(` ... and ${result.trades.length - 50} earlier trades`);
4515
+ }
4516
+ lines.push("");
4517
+ }
4136
4518
  if (result.equityCurve.length >= 3) {
4137
4519
  lines.push(` ${thinDivider}`);
4138
4520
  lines.push(" EQUITY CURVE");
@@ -9610,7 +9992,7 @@ function loadSecureEnv(basePath, passphrase) {
9610
9992
  }
9611
9993
 
9612
9994
  // src/index.ts
9613
- var AGENT_VERSION = "0.1.47";
9995
+ var AGENT_VERSION = "0.1.48";
9614
9996
  // Annotate the CommonJS export names for ESM import in node:
9615
9997
  0 && (module.exports = {
9616
9998
  AGENT_VERSION,
package/dist/index.mjs CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  tradeIdToBytes32,
63
63
  validateConfig,
64
64
  validateStrategy
65
- } from "./chunk-ICTDJ7VX.mjs";
65
+ } from "./chunk-V5XCNJWM.mjs";
66
66
  export {
67
67
  AGENT_VERSION,
68
68
  AgentConfigSchema,