Skip to content

Commit 9739186

Browse files
authored
refactor: bot issues (#15)
1 parent 48f8b28 commit 9739186

File tree

6 files changed

+688
-997
lines changed

6 files changed

+688
-997
lines changed

cow-trader/.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.11
1+
3.10

cow-trader/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,23 @@
22

33
Automated CoW Swap trading agent built with Silverback SDK
44

5-
## Run
5+
## Getting Started
6+
7+
This project utilises uv, see [docs](https://docs.astral.sh/uv/getting-started/) for installation.
8+
9+
Create venv and install dependencies:
10+
11+
```bash
12+
uv sync --all-extras --dev
13+
```
14+
15+
Install all Ape framework plugins:
16+
17+
```bash
18+
ape plugins install .
19+
```
20+
21+
## Run Silverback Bot
622

723
```bash
824
silverback run --network gnosis:mainnet:alchemy --account cow-agent

cow-trader/bot.py

Lines changed: 164 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
from dataclasses import dataclass
55
from pathlib import Path
6-
from typing import Dict, List
6+
from typing import Annotated, Dict, List
77

88
import click
99
import numpy as np
@@ -16,6 +16,7 @@
1616
from pydantic import BaseModel
1717
from pydantic_ai import Agent, RunContext
1818
from silverback import SilverbackBot, StateSnapshot
19+
from taskiq import Context, TaskiqDepends, TaskiqState
1920

2021
# Initialize bot
2122
bot = SilverbackBot()
@@ -230,11 +231,11 @@ def get_token_name(address: str) -> str:
230231
raise
231232

232233

233-
@trading_agent.tool_plain(retries=3)
234-
def get_eligible_buy_tokens(ctx: AgentDependencies) -> List[str]:
234+
@trading_agent.tool(retries=3)
235+
def get_eligible_buy_tokens(ctx: RunContext[AgentDependencies]) -> List[str]:
235236
"""Return a list of tokens eligible for purchase (excluding the sell token)."""
236237
try:
237-
sell_token = ctx.sell_token
238+
sell_token = ctx.deps.sell_token
238239
return [token for token in MONITORED_TOKENS if token != sell_token]
239240
except Exception as e:
240241
print(f"[get_eligible_buy_tokens] failed with error: {e}")
@@ -304,11 +305,10 @@ def _get_token_balances() -> Dict[str, int]:
304305
return {token_address: balance for token_address, balance in zip(MONITORED_TOKENS, results)}
305306

306307

307-
def _create_trade_context(lookback_blocks: int = 15000) -> TradeContext:
308+
def _create_trade_context(
309+
trades_df: pd.DataFrame, decisions_df: pd.DataFrame, lookback_blocks: int = 15000
310+
) -> TradeContext:
308311
"""Create TradeContext with all required data"""
309-
trades_df = _load_trades_db()
310-
decisions_df = _load_decisions_db()
311-
312312
prior_decisions = decisions_df.tail(3).copy()
313313
prior_decisions["metrics_snapshot"] = prior_decisions["metrics_snapshot"].apply(json.loads)
314314

@@ -383,32 +383,36 @@ def _save_decision(decision: AgentDecision) -> pd.DataFrame:
383383
return decisions_df
384384

385385

386-
def _update_latest_decision_outcome(final_price: float) -> pd.DataFrame:
386+
def _update_latest_decision_outcome(
387+
decisions_df: pd.DataFrame, final_price: float | None = None
388+
) -> pd.DataFrame:
387389
"""Update most recent decision with outcome data"""
388-
decisions_df = _load_decisions_db()
389-
390390
if decisions_df.empty:
391391
return decisions_df
392392

393393
latest_idx = decisions_df.index[-1]
394394
latest_decision = decisions_df.iloc[-1]
395395

396396
if latest_decision.should_trade:
397-
metrics = json.loads(latest_decision.metrics_snapshot)
398-
initial_price = next(
399-
m["last_price"]
400-
for m in metrics
401-
if m["token_a"] == latest_decision.sell_token
402-
and m["token_b"] == latest_decision.buy_token
403-
)
397+
if final_price is None:
398+
return decisions_df
399+
else:
400+
metrics = json.loads(latest_decision.metrics_snapshot)
401+
initial_price = next(
402+
m["last_price"]
403+
for m in metrics
404+
if m["token_a"] == latest_decision.sell_token
405+
and m["token_b"] == latest_decision.buy_token
406+
)
404407

405-
profitable = (
406-
final_price > initial_price
407-
if latest_decision.sell_token == metrics[0]["token_a"]
408-
else final_price < initial_price
409-
)
408+
profitable = (
409+
final_price > initial_price
410+
if latest_decision.sell_token == metrics[0]["token_a"]
411+
else final_price < initial_price
412+
)
413+
414+
decisions_df.loc[latest_idx, "profitable"] = int(profitable)
410415

411-
decisions_df.loc[latest_idx, "profitable"] = profitable
412416
_save_decisions_db(decisions_df)
413417

414418
return decisions_df
@@ -452,20 +456,20 @@ def _save_trades_db(trades_dict: Dict) -> None:
452456
df.to_csv(TRADE_FILEPATH, index=False)
453457

454458

455-
def _load_block_db() -> Dict:
459+
def _load_block_db() -> int:
456460
"""Load the last processed block from CSV file or create new if doesn't exist"""
457461
df = (
458462
pd.read_csv(BLOCK_FILEPATH)
459463
if os.path.exists(BLOCK_FILEPATH)
460464
else pd.DataFrame({"last_processed_block": [START_BLOCK]})
461465
)
462-
return {"last_processed_block": df["last_processed_block"].iloc[0]}
466+
return df["last_processed_block"].iloc[0]
463467

464468

465-
def _save_block_db(data: Dict):
469+
def _save_block_db(block_number: int) -> None:
466470
"""Save the last processed block to CSV file"""
467471
os.makedirs(os.path.dirname(BLOCK_FILEPATH), exist_ok=True)
468-
df = pd.DataFrame([data])
472+
df = pd.DataFrame({"last_processed_block": [block_number]})
469473
df.to_csv(BLOCK_FILEPATH, index=False)
470474

471475

@@ -783,95 +787,183 @@ def bot_startup(startup_state: StateSnapshot):
783787
if PROMPT_AUTOSIGN and click.confirm("Enable autosign?"):
784788
bot.signer.set_autosign(enabled=True)
785789

790+
# Process historical trades
786791
block_db = _load_block_db()
787-
last_processed_block = block_db["last_processed_block"]
788-
_save_block_db({"last_processed_block": chain.blocks.head.number})
789-
792+
last_processed_block = block_db
793+
_save_block_db(chain.blocks.head.number)
790794
_process_historical_trades(
791795
GPV2_SETTLEMENT_CONTRACT,
792796
start_block=last_processed_block,
793797
stop_block=chain.blocks.head.number,
794798
)
795799

796-
bot.state.agent = trading_agent
800+
# Initialize bot state
797801

798802
decisions_df = _load_decisions_db()
799803
if decisions_df.empty:
800804
bot.state.next_decision_block = chain.blocks.head.number
801805
else:
802806
bot.state.next_decision_block = decisions_df.iloc[-1].block_number + TRADING_BLOCK_COOLDOWN
803807

808+
bot.state.can_trade = False
809+
bot.state.sell_token = None
810+
804811
return {"message": "Starting...", "block_number": startup_state.last_block_seen}
805812

806813

807-
@bot.on_(chain.blocks)
808-
def exec_block(block: BlockAPI):
809-
"""Execute block handler with structured decision flow"""
810-
_save_block_db({"last_processed_block": block.number})
814+
@bot.on_worker_startup()
815+
def worker_startup(state: TaskiqState):
816+
"""Initialize worker state"""
817+
state.agent = trading_agent
818+
state.trades_df = _load_trades_db()
819+
state.decisions_df = _load_decisions_db()
811820

812-
if (bot.state.next_decision_block - block.number) <= 5:
813-
_catch_up_trades(
814-
current_block=block.number, next_decision_block=bot.state.next_decision_block
815-
)
816821

817-
decisions_df = _load_decisions_db()
822+
@bot.on_(chain.blocks)
823+
def update_state(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
824+
"""Update trade history and decision outcomes"""
825+
click.echo(f"\n[{block.number}] Starting state update...")
826+
_save_block_db(block.number)
827+
bot.state.can_trade = False
828+
click.echo(f"[{block.number}] State: trade={bot.state.can_trade}, sell={bot.state.sell_token}")
829+
830+
if block.number < bot.state.next_decision_block:
831+
click.echo(f"[{block.number}] Skip - next decision at {bot.state.next_decision_block}")
832+
return {"message": "Skipped - before cooldown", "block": block.number}
833+
834+
click.echo(f"[{block.number}] Past cooldown, catching up trades...")
835+
_catch_up_trades(current_block=block.number, next_decision_block=bot.state.next_decision_block)
836+
837+
bot.state.sell_token = _select_sell_token()
838+
click.echo(f"[{block.number}] Sell token: {bot.state.sell_token}")
839+
840+
if not bot.state.sell_token:
841+
click.echo(f"[{block.number}] No eligible sell tokens found")
842+
return {"message": "No eligible sell tokens", "block": block.number}
843+
844+
if context.state.decisions_df.empty:
845+
click.echo(f"[{block.number}] No previous decisions, enabling trading")
846+
bot.state.can_trade = True
847+
return {"message": "No previous decisions", "can_trade": True}
848+
849+
latest_decision = context.state.decisions_df.iloc[-1]
850+
msg = (
851+
f"[{block.number}] Latest: "
852+
f"trade={latest_decision.should_trade}, "
853+
f"block={latest_decision.block_number}"
854+
)
855+
click.echo(msg)
818856

819-
if decisions_df.empty:
820-
should_trade = True
821-
latest_decision = None
822-
else:
823-
latest_decision = decisions_df.iloc[-1]
824-
should_trade = block.number - latest_decision.block_number >= TRADING_BLOCK_COOLDOWN
857+
if not latest_decision.should_trade:
858+
click.echo(f"[{block.number}] Last decision wasn't a trade, enabling trading")
859+
bot.state.can_trade = True
860+
return {
861+
"message": "Last decision was not a trade",
862+
"can_trade": True,
863+
"last_decision_block": latest_decision.block_number,
864+
}
825865

826-
if not should_trade:
827-
return
866+
click.echo(f"[{block.number}] Creating trade context for outcome update...")
867+
trade_ctx = _create_trade_context(
868+
trades_df=context.state.trades_df, decisions_df=context.state.decisions_df
869+
)
828870

829-
trade_ctx = _create_trade_context()
871+
matching_metrics = [
872+
m.last_price
873+
for m in trade_ctx.metrics
874+
if m.token_a == latest_decision.sell_token and m.token_b == latest_decision.buy_token
875+
]
830876

831-
if latest_decision is not None and latest_decision.should_trade:
832-
_update_latest_decision_outcome(
833-
final_price=next(
834-
m.last_price
835-
for m in trade_ctx.metrics
836-
if m.token_a == latest_decision.sell_token
837-
and m.token_b == latest_decision.buy_token
838-
)
877+
if not matching_metrics:
878+
click.echo(
879+
f"[{block.number}] No metrics {latest_decision.sell_token}-{latest_decision.buy_token}"
839880
)
881+
context.state.decisions_df = _update_latest_decision_outcome(
882+
decisions_df=context.state.decisions_df,
883+
final_price=None,
884+
)
885+
bot.state.can_trade = True
886+
return {
887+
"message": "Marked as unknown outcome",
888+
"block": block.number,
889+
"can_trade": True,
890+
}
891+
892+
click.echo(f"[{block.number}] Updating previous decision outcome...")
893+
context.state.decisions_df = _update_latest_decision_outcome(
894+
decisions_df=context.state.decisions_df, final_price=matching_metrics[0]
895+
)
896+
897+
bot.state.can_trade = True
898+
click.echo(f"[{block.number}] State: trade={bot.state.can_trade}, sell={bot.state.sell_token}")
899+
return {
900+
"message": "Updated previous decision outcome",
901+
"can_trade": True,
902+
"last_decision_block": latest_decision.block_number,
903+
}
904+
840905

841-
sell_token = _select_sell_token()
842-
deps = AgentDependencies(trade_ctx=trade_ctx, sell_token=sell_token)
906+
@bot.on_(chain.blocks)
907+
def make_trading_decision(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
908+
"""Make and execute trading decisions"""
909+
click.echo(f"\n[{block.number}] Starting trading decision...")
910+
click.echo(f"[{block.number}] State: trade={bot.state.can_trade}, sell={bot.state.sell_token}")
911+
912+
if not bot.state.can_trade:
913+
click.echo(f"[{block.number}] Trading not enabled, skipping")
914+
return {"message": "Trading not enabled", "block": block.number}
915+
916+
click.echo(f"[{block.number}] Creating trade context...")
917+
trade_ctx = _create_trade_context(
918+
trades_df=context.state.trades_df, decisions_df=context.state.decisions_df
919+
)
920+
921+
click.echo(f"[{block.number}] Running agent with sell_token={bot.state.sell_token}...")
922+
deps = AgentDependencies(trade_ctx=trade_ctx, sell_token=bot.state.sell_token)
843923

844924
loop = asyncio.new_event_loop()
845925
asyncio.set_event_loop(loop)
846926

847-
try:
848-
result = bot.state.agent.run_sync(
849-
"Analyze current market conditions and make a trading decision", deps=deps
850-
)
851-
except Exception as e:
852-
click.echo(f"Anthropic API error at block {block.number}: {str(e)}")
853-
return
927+
result = context.state.agent.run_sync(
928+
"Analyze current market conditions and make a trading decision", deps=deps
929+
)
854930

931+
click.echo(
932+
f"[{block.number}] Agent: trade={result.data.should_trade}, buy={result.data.buy_token}"
933+
)
855934
_save_reasoning(block.number, result.data.reasoning)
856935

857936
decision = _build_decision(
858937
block_number=block.number,
859938
response=result.data,
860939
metrics=trade_ctx.metrics,
861-
sell_token=sell_token,
940+
sell_token=bot.state.sell_token,
862941
)
863942

864943
decision.valid = _validate_decision(decision)
944+
click.echo(f"[{block.number}] Decision valid={decision.valid}")
865945
_save_decision(decision)
866-
bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN
867946

868-
if decision.valid:
947+
if decision.valid and decision.should_trade:
948+
click.echo(f"[{block.number}] Order: {decision.sell_token} -> {decision.buy_token}")
869949
order_uid, error = create_submit_and_sign_order(
870950
sell_token=decision.sell_token,
871951
buy_token=decision.buy_token,
872952
sell_amount=trade_ctx.token_balances[decision.sell_token],
873953
)
874954
if error:
875-
click.echo(f"Order failed: {error}")
955+
click.echo(f"[{block.number}] Order failed: {error}")
876956
else:
877-
click.echo(f"Order submitted successfully. UID: {order_uid}")
957+
click.echo(f"[{block.number}] Order: {order_uid}")
958+
959+
bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN
960+
click.echo(f"[{block.number}] Next decision: {bot.state.next_decision_block}")
961+
962+
return {
963+
"message": "Trading decision made",
964+
"block": block.number,
965+
"should_trade": decision.should_trade,
966+
"sell_token": decision.sell_token,
967+
"buy_token": decision.buy_token,
968+
"next_decision_block": bot.state.next_decision_block,
969+
}

cow-trader/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ name = "cow-agent"
33
version = "0.1.0"
44
description = "Automated CoW Swap trading agent built with Silverback SDK"
55
readme = "README.md"
6-
requires-python = ">=3.11"
6+
requires-python = ">=3.10,<3.11"
77
dependencies = ["eth-ape>=0.8.25", "pydantic-ai>=0.0.23", "silverback>=0.7.0"]
88

99
[tool.uv]
1010
dev-dependencies = ["pre-commit>=4.0.1", "ruff>=0.9.1"]
1111

1212
[tool.ruff]
1313
line-length = 100
14-
target-version = "py311"
14+
target-version = "py310"
1515

1616
[tool.ruff.lint]
1717
select = [

0 commit comments

Comments
 (0)