Skip to content

Commit 3537f1e

Browse files
authored
feat: catch up logs just in time for agent decision (#11)
1 parent 21edd12 commit 3537f1e

File tree

2 files changed

+134
-59
lines changed

2 files changed

+134
-59
lines changed

cow-trader/bot.py

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import json
33
import os
44
from pathlib import Path
5-
from typing import Annotated, Dict, List
5+
from typing import Dict, List
66

77
import click
8+
import numpy as np
89
import pandas as pd
910
import requests
1011
from ape import Contract, accounts, chain
@@ -14,7 +15,6 @@
1415
from pydantic import BaseModel
1516
from pydantic_ai import Agent
1617
from silverback import SilverbackBot, StateSnapshot
17-
from taskiq import Context, TaskiqDepends
1818

1919
# Initialize bot
2020
bot = SilverbackBot()
@@ -81,11 +81,13 @@ class TradeMetrics(BaseModel):
8181
max_price: float
8282
volume_buy: float
8383
volume_sell: float
84-
order_imbalance: float
84+
up_moves_ratio: float
85+
max_up_streak: int
86+
max_down_streak: int
8587
trade_count: int
8688

8789

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]:
8991
"""Compute trading metrics for all token pairs in filtered DataFrame"""
9092
if df.empty:
9193
return []
@@ -100,28 +102,66 @@ def compute_metrics(df: pd.DataFrame, lookback_blocks: int = 15000) -> List[Trad
100102
metrics_list = []
101103

102104
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
125165

126166
return metrics_list
127167

@@ -197,14 +237,23 @@ def create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
197237
trades_df = _load_trades_db()
198238
decisions_df = _load_decisions_db()
199239

240+
prior_decisions = decisions_df.tail(3).copy()
241+
prior_decisions["metrics_snapshot"] = prior_decisions["metrics_snapshot"].apply(json.loads)
242+
200243
return TradeContext(
201244
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"),
204247
lookback_blocks=lookback_blocks,
205248
)
206249

207250

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+
208257
def _build_decision(
209258
block_number: int, response: AgentResponse, metrics: List[TradeMetrics]
210259
) -> AgentDecision:
@@ -481,6 +530,23 @@ def _process_historical_trades(
481530
return trades
482531

483532

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+
484550
def _extend_historical_trades() -> None:
485551
"""Extend trades.csv data further back in history"""
486552
trades_df = _load_trades_db()
@@ -642,24 +708,35 @@ def bot_startup(startup_state: StateSnapshot):
642708
"""Initialize bot state and historical data"""
643709
block_db = _load_block_db()
644710
last_processed_block = block_db["last_processed_block"]
711+
_save_block_db({"last_processed_block": chain.blocks.head.number})
645712

646713
_process_historical_trades(
647714
GPV2_SETTLEMENT_CONTRACT,
648715
start_block=last_processed_block,
649716
stop_block=chain.blocks.head.number,
650717
)
651718

652-
_save_block_db({"last_processed_block": chain.blocks.head.number})
653-
bot.state.last_extension_block = chain.blocks.head.number
654719
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+
655727
return {"message": "Starting...", "block_number": startup_state.last_block_seen}
656728

657729

658730
@bot.on_(chain.blocks)
659-
def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
731+
def exec_block(block: BlockAPI):
660732
"""Execute block handler with structured decision flow"""
661733
_save_block_db({"last_processed_block": block.number})
662734

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+
663740
decisions_df = _load_decisions_db()
664741

665742
if decisions_df.empty:
@@ -703,6 +780,7 @@ def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
703780

704781
decision.valid = _validate_decision(decision, trade_ctx)
705782
_save_decision(decision)
783+
bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN
706784

707785
if decision.valid:
708786
order_uid, error = create_and_submit_order(

cow-trader/system_prompt.txt

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
11
You are an analytical trading system that autonomously evaluates market
22
conditions and historical trade outcomes for CoW Swap.
33

4-
Analysis:
5-
- Evaluate price trends, trade volumes, order imbalances, and trade counts.
6-
- Determine whether current market conditions favor actively trading volatile
7-
token pairs for higher risk/reward, or suggest a safe exit (i.e., staying out
8-
of the volatile market by opting for stable coins).
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()
99

10-
Before trading:
11-
- Always convert token addresses to human-readable names using get_token_name().
12-
- Analyze key metrics for all monitored tokens:
13-
- Price trends and volatility
14-
- Trading volumes (buy/sell)
15-
- Order imbalances
16-
- Recent trade counts
17-
- Assess whether the market favours trading volatile tokens or seeking stable
18-
coin safety.
19-
- Review prior trade outcomes to identify successful patterns.
20-
- When executing a trade, sell your entire position of the selected token and
21-
convert it completely to the specified counter token.
22-
- Trading decisions occur every 360 blocks (~30 minutes).
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
2315

24-
Trading Decision:
25-
- You have three paths:
26-
1. Do not trade.
27-
2. Trade volatile tokens (GNO, COW, SAFE, WETH).
28-
3. Trade a stable coin (WXDAI).
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
2920

30-
Decide:
31-
- Should we trade? (boolean decision)
32-
- If yes, which token to sell and which token to buy (provide actual addresses).
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)
3327

34-
Reasoning must include only the relevant quantitative metrics. No general market
35-
commentary or disclaimers.
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

0 commit comments

Comments
 (0)