Skip to content

Commit 206051b

Browse files
authored
refactor: agent tooling (#12)
1 parent 3537f1e commit 206051b

File tree

2 files changed

+145
-104
lines changed

2 files changed

+145
-104
lines changed

cow-trader/bot.py

Lines changed: 104 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import json
33
import os
4+
from dataclasses import dataclass
45
from pathlib import Path
56
from typing import Dict, List
67

@@ -13,7 +14,7 @@
1314
from ape.types import LogFilter
1415
from ape_ethereum import multicall
1516
from pydantic import BaseModel
16-
from pydantic_ai import Agent
17+
from pydantic_ai import Agent, RunContext
1718
from silverback import SilverbackBot, StateSnapshot
1819

1920
# Initialize bot
@@ -33,16 +34,12 @@
3334

3435
GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
3536
COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
36-
WETH = "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1"
37-
SAFE = "0x4d18815D14fe5c3304e87B3FA18318baa5c23820"
3837
WXDAI = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
39-
MONITORED_TOKENS = [GNO, COW, WETH, SAFE, WXDAI]
38+
MONITORED_TOKENS = [GNO, COW, WXDAI]
4039

4140
MINIMUM_TOKEN_BALANCES = {
4241
GNO: 5e16,
4342
COW: 25e18,
44-
WETH: 38e14,
45-
SAFE: 15e18,
4643
WXDAI: 10e18,
4744
}
4845

@@ -175,11 +172,18 @@ class TradeContext(BaseModel):
175172
lookback_blocks: int = 15000
176173

177174

175+
@dataclass
176+
class AgentDependencies:
177+
"""Dependencies for trading agent"""
178+
179+
trade_ctx: TradeContext
180+
sell_token: str | None
181+
182+
178183
class AgentResponse(BaseModel):
179184
"""Structured response from agent"""
180185

181186
should_trade: bool
182-
sell_token: str | None = None
183187
buy_token: str | None = None
184188
reasoning: str
185189

@@ -198,24 +202,74 @@ class AgentDecision(BaseModel):
198202

199203
trading_agent = Agent(
200204
"anthropic:claude-3-sonnet-20240229",
201-
deps_type=TradeContext,
205+
deps_type=AgentDependencies,
202206
result_type=AgentResponse,
203207
system_prompt=SYSTEM_PROMPT,
204208
)
205209

206210
TOKEN_NAMES = {
207-
"0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb": "GNO",
208-
"0x177127622c4A00F3d409B75571e12cB3c8973d3c": "COW",
209-
"0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1": "WETH",
210-
"0x4d18815D14fe5c3304e87B3FA18318baa5c23820": "SAFE",
211-
"0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d": "WXDAI",
211+
GNO: "GNO",
212+
COW: "COW",
213+
WXDAI: "WXDAI",
212214
}
213215

214216

215-
@trading_agent.tool_plain
217+
@trading_agent.tool_plain(retries=3)
216218
def get_token_name(address: str) -> str:
217-
"""Get human readable token name from contract address"""
218-
return TOKEN_NAMES.get(address, address)
219+
"""Return a human-readable token name for the provided address."""
220+
try:
221+
return TOKEN_NAMES.get(address, address)
222+
except Exception as e:
223+
print(f"[get_token_name] failed with error: {e}")
224+
raise
225+
226+
227+
@trading_agent.tool_plain(retries=3)
228+
def get_eligible_buy_tokens(ctx: AgentDependencies) -> List[str]:
229+
"""Return a list of tokens eligible for purchase (excluding the sell token)."""
230+
try:
231+
sell_token = ctx.sell_token
232+
return [token for token in MONITORED_TOKENS if token != sell_token]
233+
except Exception as e:
234+
print(f"[get_eligible_buy_tokens] failed with error: {e}")
235+
raise
236+
237+
238+
@trading_agent.tool_plain(retries=3)
239+
def get_token_type(token: str) -> Dict:
240+
"""Determine if the token is stable or volatile."""
241+
try:
242+
is_stable = token == WXDAI
243+
return {
244+
"token": get_token_name(token),
245+
"is_stable": is_stable,
246+
"expected_behavior": "USD value stable, good for preserving value"
247+
if is_stable
248+
else "USD value can fluctuate",
249+
}
250+
except Exception as e:
251+
print(f"[get_token_type] failed with error: {e}")
252+
raise
253+
254+
255+
@trading_agent.tool(retries=3)
256+
def get_trading_context(ctx: RunContext[AgentDependencies]) -> TradeContext:
257+
"""Return the trading context from the agent's dependencies."""
258+
try:
259+
return ctx.deps.trade_ctx
260+
except Exception as e:
261+
print(f"[get_trading_context] failed with error: {e}")
262+
raise
263+
264+
265+
@trading_agent.tool(retries=3)
266+
def get_sell_token(ctx: RunContext[AgentDependencies]) -> str | None:
267+
"""Return the sell token from the agent's dependencies."""
268+
try:
269+
return ctx.deps.sell_token
270+
except Exception as e:
271+
print(f"[get_sell_token] failed with error: {e}")
272+
raise
219273

220274

221275
def _get_token_balances() -> Dict[str, int]:
@@ -231,8 +285,7 @@ def _get_token_balances() -> Dict[str, int]:
231285
return {token_address: balance for token_address, balance in zip(MONITORED_TOKENS, results)}
232286

233287

234-
@trading_agent.tool_plain
235-
def create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
288+
def _create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
236289
"""Create TradeContext with all required data"""
237290
trades_df = _load_trades_db()
238291
decisions_df = _load_decisions_db()
@@ -248,54 +301,45 @@ def create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
248301
)
249302

250303

251-
@trading_agent.tool_plain
252-
def get_minimum_token_balances() -> Dict[str, float]:
253-
"""Get dictionary of minimum required balances for all monitored tokens"""
254-
return {addr: float(amount) for addr, amount in MINIMUM_TOKEN_BALANCES.items()}
304+
def _select_sell_token() -> str | None:
305+
"""
306+
Select token to sell based on current balances and minimum thresholds.
307+
Returns the token address that has a balance above threshold, or None if no token qualifies.
308+
"""
309+
balances = _get_token_balances()
310+
valid_tokens = [
311+
token for token in MONITORED_TOKENS if balances[token] > MINIMUM_TOKEN_BALANCES[token]
312+
]
313+
return valid_tokens[0] if valid_tokens else None
255314

256315

257316
def _build_decision(
258-
block_number: int, response: AgentResponse, metrics: List[TradeMetrics]
317+
block_number: int,
318+
response: AgentResponse,
319+
metrics: List[TradeMetrics],
320+
sell_token: str,
259321
) -> AgentDecision:
260-
"""Build structured AgentDecision from raw response"""
322+
"""Build decision dict from agent response"""
261323
return AgentDecision(
262324
block_number=block_number,
263325
should_trade=response.should_trade,
264-
sell_token=response.sell_token,
265-
buy_token=response.buy_token,
326+
sell_token=sell_token if response.should_trade else None,
327+
buy_token=response.buy_token if response.should_trade else None,
266328
metrics_snapshot=metrics,
267-
profitable=2,
329+
reasoning=response.reasoning,
268330
valid=False,
269331
)
270332

271333

272-
def _validate_decision(decision: AgentDecision, trade_context: TradeContext) -> bool:
334+
def _validate_decision(decision: AgentDecision) -> bool:
273335
"""
274-
Validate decision structure, token validity, and balance requirements
275-
Args:
276-
decision: The trading decision to validate
277-
trade_context: Current trading context with balances and metrics
278-
Returns:
279-
bool: True if valid, False otherwise
336+
Validate decision structure and buy token validity
280337
"""
281-
if not decision.should_trade:
282-
return True
283-
284-
if (
285-
decision.sell_token not in MONITORED_TOKENS
286-
or decision.buy_token not in MONITORED_TOKENS
287-
or decision.sell_token == decision.buy_token
288-
):
289-
click.echo(f"Invalid token pair: sell={decision.sell_token}, buy={decision.buy_token}")
338+
if not decision.should_trade or decision.sell_token is None or decision.buy_token is None:
290339
return False
291340

292-
sell_balance = trade_context.token_balances[decision.sell_token]
293-
min_balance = MINIMUM_TOKEN_BALANCES[decision.sell_token]
294-
295-
if sell_balance < min_balance:
296-
click.echo(
297-
f"Insufficient balance for {decision.sell_token}: {sell_balance} < {min_balance}"
298-
)
341+
if decision.buy_token not in MONITORED_TOKENS or decision.buy_token == decision.sell_token:
342+
click.echo(f"Invalid buy token: buy={decision.buy_token}")
299343
return False
300344

301345
return True
@@ -547,28 +591,6 @@ def _catch_up_trades(current_block: int, next_decision_block: int, buffer_blocks
547591
)
548592

549593

550-
def _extend_historical_trades() -> None:
551-
"""Extend trades.csv data further back in history"""
552-
trades_df = _load_trades_db()
553-
554-
if len(trades_df) == 0:
555-
oldest_block = chain.blocks.head.number
556-
else:
557-
oldest_block = trades_df["block_number"].min()
558-
559-
new_trades = _process_historical_trades(
560-
GPV2_SETTLEMENT_CONTRACT,
561-
start_block=oldest_block - HISTORICAL_BLOCK_STEP,
562-
stop_block=oldest_block - 1,
563-
)
564-
565-
new_trades_df = pd.DataFrame(new_trades)
566-
all_trades = pd.concat([new_trades_df, trades_df])
567-
all_trades = all_trades.sort_values("block_number", ascending=True)
568-
569-
_save_trades_db(all_trades)
570-
571-
572594
# CoW Swap trading helper functions
573595
def _construct_quote_payload(
574596
sell_token: str,
@@ -749,7 +771,7 @@ def exec_block(block: BlockAPI):
749771
if not should_trade:
750772
return
751773

752-
trade_ctx = create_trade_context()
774+
trade_ctx = _create_trade_context()
753775

754776
if latest_decision is not None and latest_decision.should_trade:
755777
_update_latest_decision_outcome(
@@ -761,12 +783,15 @@ def exec_block(block: BlockAPI):
761783
)
762784
)
763785

786+
sell_token = _select_sell_token()
787+
deps = AgentDependencies(trade_ctx=trade_ctx, sell_token=sell_token)
788+
764789
loop = asyncio.new_event_loop()
765790
asyncio.set_event_loop(loop)
766791

767792
try:
768793
result = bot.state.agent.run_sync(
769-
"Analyze current market conditions and make a trading decision", deps=trade_ctx
794+
"Analyze current market conditions and make a trading decision", deps=deps
770795
)
771796
except Exception as e:
772797
click.echo(f"Anthropic API error at block {block.number}: {str(e)}")
@@ -775,10 +800,13 @@ def exec_block(block: BlockAPI):
775800
_save_reasoning(block.number, result.data.reasoning)
776801

777802
decision = _build_decision(
778-
block_number=block.number, response=result.data, metrics=trade_ctx.metrics
803+
block_number=block.number,
804+
response=result.data,
805+
metrics=trade_ctx.metrics,
806+
sell_token=sell_token,
779807
)
780808

781-
decision.valid = _validate_decision(decision, trade_ctx)
809+
decision.valid = _validate_decision(decision)
782810
_save_decision(decision)
783811
bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN
784812

cow-trader/system_prompt.txt

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,45 @@
1-
You are an analytical trading system that autonomously evaluates market
2-
conditions and historical trade outcomes for CoW Swap.
1+
You are a trading bot on CoW Swap DEX.
2+
Your job is to analyze market conditions and decide whether to trade.
33

4-
Data Analysis Steps:
5-
1. Check Current Balances (from token_balances):
6-
- Get current token balances and minimum thresholds
7-
- Only consider tokens where balance > minimum required
8-
- Convert addresses to names using get_token_name()
4+
CONTEXT ACCESS:
5+
- get_trading_context(): Returns a TradeContext containing:
6+
- token_balances: Dict[str, str] — Your current token holdings.
7+
- metrics: A list of TradeMetrics for each trading pair, where:
8+
• token_a, token_b: The addresses of the tokens in the pair.
9+
• last_price: The most recent trade price, computed as (value of token_a) / (value of token_b).
10+
• min_price: The lowest observed price (token_a/token_b) over the lookback period.
11+
• max_price: The highest observed price (token_a/token_b) over the lookback period.
12+
• volume_buy: The total buy volume for the pair over the lookback period.
13+
• volume_sell: The total sell volume for the pair over the lookback period.
14+
• trade_count: The total number of trades executed during the lookback period.
15+
• up_moves_ratio: The fraction of trades where the price moved upward.
16+
• max_up_streak: The longest consecutive streak of upward price moves.
17+
• max_down_streak: The longest consecutive streak of downward price moves.
18+
- prior_decisions: A record of previous trading decisions and their outcomes.
19+
- get_sell_token(): Returns the token you currently hold and can sell.
920

10-
2. Analyze Trade Metrics (from metrics):
11-
- Focus on pairs with sufficient balance
12-
- Review price trends, volatility
13-
- Check trading volumes and imbalances
14-
- Consider recent trade counts
21+
AVAILABLE TOOLS:
22+
- get_token_name(address): Get a human-readable token name.
23+
- get_eligible_buy_tokens(): Get a list of valid tokens you can buy.
24+
- get_token_type(token): Determine if a token is stable (like WXDAI) or volatile.
25+
- analyze_pair_stability(token_a, token_b): Understand the price relationship between tokens.
1526

16-
3. Review Prior Decisions (from prior_decisions):
17-
- Evaluate success of recent trades
18-
- Learn from profitable/unprofitable decisions
19-
- Adjust strategy based on outcomes
27+
TRADING RULES:
28+
1. When analyzing pairs:
29+
- WXDAI is a stablecoin worth $1.
30+
- Non-stablecoin prices fluctuate in USD terms.
31+
- Selling a stablecoin into a volatile token exposes you to price risk.
32+
- Buying a stablecoin preserves USD value.
33+
2. Decision making:
34+
- Use the provided metrics to assess market conditions.
35+
- Consider the profitability of prior decisions.
36+
- If trading, select a buy_token from get_eligible_buy_tokens().
37+
- If not trading, return None for buy_token.
2038

21-
Trading Decision:
22-
1. Select tokens with sufficient balance
23-
2. Choose best pair based on metrics and past performance
24-
3. Decide:
25-
- Should trade? (boolean)
26-
- If yes, which token to sell/buy (addresses)
27-
28-
Reasoning format (be concise):
29-
1. 1-2 key metrics that influenced the decision
30-
2. Your interpretation of the metrics
31-
3. Prior performance insight
32-
4. Trade decision rationale
39+
OUTPUT REQUIRED:
40+
should_trade: bool
41+
- Whether to execute a trade.
42+
buy_token: str | None
43+
- Must be a hex address from get_eligible_buy_tokens() if trading, or None if not.
44+
reasoning: str
45+
- A concise (1 short sentence) justifying your decision, referencing aspects like market analysis, prior results, risk, or other relevant factors.

0 commit comments

Comments
 (0)