2
2
import json
3
3
import os
4
4
from pathlib import Path
5
- from typing import Annotated , Dict , List
5
+ from typing import Dict , List
6
6
7
7
import click
8
+ import numpy as np
8
9
import pandas as pd
9
10
import requests
10
11
from ape import Contract , accounts , chain
14
15
from pydantic import BaseModel
15
16
from pydantic_ai import Agent
16
17
from silverback import SilverbackBot , StateSnapshot
17
- from taskiq import Context , TaskiqDepends
18
18
19
19
# Initialize bot
20
20
bot = SilverbackBot ()
@@ -81,11 +81,13 @@ class TradeMetrics(BaseModel):
81
81
max_price : float
82
82
volume_buy : float
83
83
volume_sell : float
84
- order_imbalance : float
84
+ up_moves_ratio : float
85
+ max_up_streak : int
86
+ max_down_streak : int
85
87
trade_count : int
86
88
87
89
88
- def compute_metrics (df : pd .DataFrame , lookback_blocks : int = 15000 ) -> List [TradeMetrics ]:
90
+ def _compute_metrics (df : pd .DataFrame , lookback_blocks : int = 15000 ) -> List [TradeMetrics ]:
89
91
"""Compute trading metrics for all token pairs in filtered DataFrame"""
90
92
if df .empty :
91
93
return []
@@ -100,28 +102,66 @@ def compute_metrics(df: pd.DataFrame, lookback_blocks: int = 15000) -> List[Trad
100
102
metrics_list = []
101
103
102
104
for _ , pair in pairs_df .iterrows ():
103
- pair_df = filtered_df [
104
- (filtered_df .token_a == pair .token_a ) & (filtered_df .token_b == pair .token_b )
105
- ]
106
-
107
- volume_buy = pair_df .buyAmount .astype (float ).sum ()
108
- volume_sell = pair_df .sellAmount .astype (float ).sum ()
109
-
110
- volume_sum = volume_buy + volume_sell
111
- order_imbalance = ((volume_buy - volume_sell ) / volume_sum ) if volume_sum != 0 else 0
112
-
113
- metrics = TradeMetrics (
114
- token_a = pair .token_a ,
115
- token_b = pair .token_b ,
116
- last_price = pair_df .price .iloc [- 1 ],
117
- min_price = pair_df .price .min (),
118
- max_price = pair_df .price .max (),
119
- volume_buy = volume_buy ,
120
- volume_sell = volume_sell ,
121
- order_imbalance = order_imbalance ,
122
- trade_count = len (pair_df ),
123
- )
124
- metrics_list .append (metrics )
105
+ try :
106
+ pair_df = filtered_df [
107
+ (filtered_df .token_a == pair .token_a ) & (filtered_df .token_b == pair .token_b )
108
+ ].sort_values ("block_number" )
109
+
110
+ pair_df = pair_df [pair_df .price .notna ()]
111
+
112
+ if pair_df .empty :
113
+ continue
114
+
115
+ try :
116
+ volume_buy = pair_df .buyAmount .astype (float ).sum ()
117
+ volume_sell = pair_df .sellAmount .astype (float ).sum ()
118
+ except (ValueError , TypeError ):
119
+ volume_buy = volume_sell = 0.0
120
+
121
+ prices = pair_df .price .values
122
+
123
+ up_moves_ratio = 0.5
124
+ max_up_streak = 0
125
+ max_down_streak = 0
126
+
127
+ if len (prices ) >= 2 :
128
+ price_changes = np .sign (np .diff (prices ))
129
+ non_zero_moves = price_changes [price_changes != 0 ]
130
+
131
+ if len (non_zero_moves ) > 0 :
132
+ up_moves_ratio = np .mean (non_zero_moves > 0 )
133
+
134
+ if len (price_changes ) > 1 :
135
+ try :
136
+ change_points = np .where (price_changes [1 :] != price_changes [:- 1 ])[0 ] + 1
137
+ if len (change_points ) > 0 :
138
+ streaks = np .split (price_changes , change_points )
139
+ max_up_streak = max (
140
+ (len (s ) for s in streaks if len (s ) > 0 and s [0 ] > 0 ), default = 0
141
+ )
142
+ max_down_streak = max (
143
+ (len (s ) for s in streaks if len (s ) > 0 and s [0 ] < 0 ), default = 0
144
+ )
145
+ except Exception :
146
+ pass
147
+
148
+ metrics = TradeMetrics (
149
+ token_a = pair .token_a ,
150
+ token_b = pair .token_b ,
151
+ last_price = float (prices [- 1 ]),
152
+ min_price = float (np .min (prices )),
153
+ max_price = float (np .max (prices )),
154
+ volume_buy = float (volume_buy ),
155
+ volume_sell = float (volume_sell ),
156
+ up_moves_ratio = float (up_moves_ratio ),
157
+ max_up_streak = int (max_up_streak ),
158
+ max_down_streak = int (max_down_streak ),
159
+ trade_count = len (pair_df ),
160
+ )
161
+ metrics_list .append (metrics )
162
+ except Exception as e :
163
+ click .echo (f"Error processing pair { pair .token_a } -{ pair .token_b } : { str (e )} " )
164
+ continue
125
165
126
166
return metrics_list
127
167
@@ -197,14 +237,23 @@ def create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
197
237
trades_df = _load_trades_db ()
198
238
decisions_df = _load_decisions_db ()
199
239
240
+ prior_decisions = decisions_df .tail (3 ).copy ()
241
+ prior_decisions ["metrics_snapshot" ] = prior_decisions ["metrics_snapshot" ].apply (json .loads )
242
+
200
243
return TradeContext (
201
244
token_balances = _get_token_balances (),
202
- metrics = compute_metrics (trades_df , lookback_blocks ),
203
- prior_decisions = decisions_df . tail ( 10 ) .to_dict ("records" ),
245
+ metrics = _compute_metrics (trades_df , lookback_blocks ),
246
+ prior_decisions = prior_decisions .to_dict ("records" ),
204
247
lookback_blocks = lookback_blocks ,
205
248
)
206
249
207
250
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 ()}
255
+
256
+
208
257
def _build_decision (
209
258
block_number : int , response : AgentResponse , metrics : List [TradeMetrics ]
210
259
) -> AgentDecision :
@@ -481,6 +530,23 @@ def _process_historical_trades(
481
530
return trades
482
531
483
532
533
+ def _catch_up_trades (current_block : int , next_decision_block : int , buffer_blocks : int = 5 ) -> None :
534
+ """
535
+ Catch up on trade events from last processed block until shortly before next decision
536
+ """
537
+ trades_df = _load_trades_db ()
538
+ last_processed_block = trades_df ["block_number" ].max () if not trades_df .empty else START_BLOCK
539
+
540
+ target_block = min (current_block , next_decision_block - buffer_blocks )
541
+
542
+ if target_block <= last_processed_block :
543
+ return
544
+
545
+ _process_historical_trades (
546
+ GPV2_SETTLEMENT_CONTRACT , start_block = last_processed_block + 1 , stop_block = target_block
547
+ )
548
+
549
+
484
550
def _extend_historical_trades () -> None :
485
551
"""Extend trades.csv data further back in history"""
486
552
trades_df = _load_trades_db ()
@@ -642,24 +708,35 @@ def bot_startup(startup_state: StateSnapshot):
642
708
"""Initialize bot state and historical data"""
643
709
block_db = _load_block_db ()
644
710
last_processed_block = block_db ["last_processed_block" ]
711
+ _save_block_db ({"last_processed_block" : chain .blocks .head .number })
645
712
646
713
_process_historical_trades (
647
714
GPV2_SETTLEMENT_CONTRACT ,
648
715
start_block = last_processed_block ,
649
716
stop_block = chain .blocks .head .number ,
650
717
)
651
718
652
- _save_block_db ({"last_processed_block" : chain .blocks .head .number })
653
- bot .state .last_extension_block = chain .blocks .head .number
654
719
bot .state .agent = trading_agent
720
+
721
+ decisions_df = _load_decisions_db ()
722
+ if decisions_df .empty :
723
+ bot .state .next_decision_block = chain .blocks .head .number
724
+ else :
725
+ bot .state .next_decision_block = decisions_df .iloc [- 1 ].block_number + TRADING_BLOCK_COOLDOWN
726
+
655
727
return {"message" : "Starting..." , "block_number" : startup_state .last_block_seen }
656
728
657
729
658
730
@bot .on_ (chain .blocks )
659
- def exec_block (block : BlockAPI , context : Annotated [ Context , TaskiqDepends ()] ):
731
+ def exec_block (block : BlockAPI ):
660
732
"""Execute block handler with structured decision flow"""
661
733
_save_block_db ({"last_processed_block" : block .number })
662
734
735
+ if (bot .state .next_decision_block - block .number ) <= 5 :
736
+ _catch_up_trades (
737
+ current_block = block .number , next_decision_block = bot .state .next_decision_block
738
+ )
739
+
663
740
decisions_df = _load_decisions_db ()
664
741
665
742
if decisions_df .empty :
@@ -703,6 +780,7 @@ def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
703
780
704
781
decision .valid = _validate_decision (decision , trade_ctx )
705
782
_save_decision (decision )
783
+ bot .state .next_decision_block = block .number + TRADING_BLOCK_COOLDOWN
706
784
707
785
if decision .valid :
708
786
order_uid , error = create_and_submit_order (
0 commit comments