1
1
import asyncio
2
2
import json
3
3
import os
4
+ from dataclasses import dataclass
4
5
from pathlib import Path
5
6
from typing import Dict , List
6
7
13
14
from ape .types import LogFilter
14
15
from ape_ethereum import multicall
15
16
from pydantic import BaseModel
16
- from pydantic_ai import Agent
17
+ from pydantic_ai import Agent , RunContext
17
18
from silverback import SilverbackBot , StateSnapshot
18
19
19
20
# Initialize bot
33
34
34
35
GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
35
36
COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
36
- WETH = "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1"
37
- SAFE = "0x4d18815D14fe5c3304e87B3FA18318baa5c23820"
38
37
WXDAI = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
39
- MONITORED_TOKENS = [GNO , COW , WETH , SAFE , WXDAI ]
38
+ MONITORED_TOKENS = [GNO , COW , WXDAI ]
40
39
41
40
MINIMUM_TOKEN_BALANCES = {
42
41
GNO : 5e16 ,
43
42
COW : 25e18 ,
44
- WETH : 38e14 ,
45
- SAFE : 15e18 ,
46
43
WXDAI : 10e18 ,
47
44
}
48
45
@@ -175,11 +172,18 @@ class TradeContext(BaseModel):
175
172
lookback_blocks : int = 15000
176
173
177
174
175
+ @dataclass
176
+ class AgentDependencies :
177
+ """Dependencies for trading agent"""
178
+
179
+ trade_ctx : TradeContext
180
+ sell_token : str | None
181
+
182
+
178
183
class AgentResponse (BaseModel ):
179
184
"""Structured response from agent"""
180
185
181
186
should_trade : bool
182
- sell_token : str | None = None
183
187
buy_token : str | None = None
184
188
reasoning : str
185
189
@@ -198,24 +202,74 @@ class AgentDecision(BaseModel):
198
202
199
203
trading_agent = Agent (
200
204
"anthropic:claude-3-sonnet-20240229" ,
201
- deps_type = TradeContext ,
205
+ deps_type = AgentDependencies ,
202
206
result_type = AgentResponse ,
203
207
system_prompt = SYSTEM_PROMPT ,
204
208
)
205
209
206
210
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" ,
212
214
}
213
215
214
216
215
- @trading_agent .tool_plain
217
+ @trading_agent .tool_plain ( retries = 3 )
216
218
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
219
273
220
274
221
275
def _get_token_balances () -> Dict [str , int ]:
@@ -231,8 +285,7 @@ def _get_token_balances() -> Dict[str, int]:
231
285
return {token_address : balance for token_address , balance in zip (MONITORED_TOKENS , results )}
232
286
233
287
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 :
236
289
"""Create TradeContext with all required data"""
237
290
trades_df = _load_trades_db ()
238
291
decisions_df = _load_decisions_db ()
@@ -248,54 +301,45 @@ def create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
248
301
)
249
302
250
303
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
255
314
256
315
257
316
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 ,
259
321
) -> AgentDecision :
260
- """Build structured AgentDecision from raw response"""
322
+ """Build decision dict from agent response"""
261
323
return AgentDecision (
262
324
block_number = block_number ,
263
325
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 ,
266
328
metrics_snapshot = metrics ,
267
- profitable = 2 ,
329
+ reasoning = response . reasoning ,
268
330
valid = False ,
269
331
)
270
332
271
333
272
- def _validate_decision (decision : AgentDecision , trade_context : TradeContext ) -> bool :
334
+ def _validate_decision (decision : AgentDecision ) -> bool :
273
335
"""
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
280
337
"""
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 :
290
339
return False
291
340
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 } " )
299
343
return False
300
344
301
345
return True
@@ -547,28 +591,6 @@ def _catch_up_trades(current_block: int, next_decision_block: int, buffer_blocks
547
591
)
548
592
549
593
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
-
572
594
# CoW Swap trading helper functions
573
595
def _construct_quote_payload (
574
596
sell_token : str ,
@@ -749,7 +771,7 @@ def exec_block(block: BlockAPI):
749
771
if not should_trade :
750
772
return
751
773
752
- trade_ctx = create_trade_context ()
774
+ trade_ctx = _create_trade_context ()
753
775
754
776
if latest_decision is not None and latest_decision .should_trade :
755
777
_update_latest_decision_outcome (
@@ -761,12 +783,15 @@ def exec_block(block: BlockAPI):
761
783
)
762
784
)
763
785
786
+ sell_token = _select_sell_token ()
787
+ deps = AgentDependencies (trade_ctx = trade_ctx , sell_token = sell_token )
788
+
764
789
loop = asyncio .new_event_loop ()
765
790
asyncio .set_event_loop (loop )
766
791
767
792
try :
768
793
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
770
795
)
771
796
except Exception as e :
772
797
click .echo (f"Anthropic API error at block { block .number } : { str (e )} " )
@@ -775,10 +800,13 @@ def exec_block(block: BlockAPI):
775
800
_save_reasoning (block .number , result .data .reasoning )
776
801
777
802
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 ,
779
807
)
780
808
781
- decision .valid = _validate_decision (decision , trade_ctx )
809
+ decision .valid = _validate_decision (decision )
782
810
_save_decision (decision )
783
811
bot .state .next_decision_block = block .number + TRADING_BLOCK_COOLDOWN
784
812
0 commit comments