@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/chunk-CVRY5TKR.mjs +10261 -0
- package/dist/chunk-D22X7J5B.mjs +10259 -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 +541 -67
- package/dist/cli.mjs +91 -1
- package/dist/index.d.mts +26 -9
- package/dist/index.d.ts +26 -9
- package/dist/index.js +448 -66
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
1965
|
+
systemPrompt: `You are an AI trading analyst specializing in momentum trading on Base network.
|
|
1909
1966
|
|
|
1910
|
-
|
|
1967
|
+
Analyze the provided market data and identify momentum-based trading opportunities.
|
|
1911
1968
|
|
|
1912
|
-
|
|
1969
|
+
RULES:
|
|
1913
1970
|
- Only recommend trades when there is clear momentum evidence
|
|
1914
|
-
-
|
|
1915
|
-
-
|
|
1916
|
-
- Be conservative with confidence
|
|
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
|
-
|
|
1919
|
-
1.
|
|
1920
|
-
2.
|
|
1921
|
-
3.
|
|
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
|
|
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
|
-
"
|
|
1930
|
-
"
|
|
1931
|
-
"
|
|
1932
|
-
"
|
|
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
|
-
|
|
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
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
-
|
|
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
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
|
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
|
-
"
|
|
1992
|
-
"
|
|
1993
|
-
"
|
|
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(
|
|
2299
|
+
{ role: 'system', content: config.strategyPrompt ?? VALUE_SYSTEM_PROMPT },
|
|
2300
|
+
{ role: 'user', content: JSON.stringify(payload) },
|
|
2006
2301
|
]);
|
|
2007
2302
|
|
|
2008
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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,
|