Skip to content

Commit 4b5b720

Browse files
authored
feat: add historical price monitoring (#9)
This PR adds historical price monitoring for trades between monitored tokens on CoW Swap. ### Key Changes - Track historical trades in `trades.csv` - Calculate token prices during trade processing - Use canonical token pair ordering ### Implementation Details - Process trade logs with price calculation - Store prices alongside original trade data - Maintain consistent token ordering for price comparisons ### Technical Notes - Prices calculated from raw amounts (18 decimals) - Token pairs ordered alphabetically by address - Historical data preserved in `trades.csv`
1 parent 7640419 commit 4b5b720

File tree

1 file changed

+95
-42
lines changed

1 file changed

+95
-42
lines changed

cow-trader/bot.py

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import json
22
import os
33
from pathlib import Path
4-
from typing import Annotated, Dict
4+
from typing import Annotated, Dict, List
55

66
import click
7-
import numpy as np
87
import pandas as pd
98
import requests
109
from ape import Contract, accounts, chain
@@ -27,8 +26,13 @@
2726
SAFE_ADDRESS = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe" # PLACEHOLDER
2827
TOKEN_ALLOWLIST_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
2928
GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
30-
GNO_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
31-
COW_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
29+
GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
30+
COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
31+
WETH = "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1"
32+
SAFE = "0x4d18815D14fe5c3304e87B3FA18318baa5c23820"
33+
WXDAI = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
34+
35+
MONITORED_TOKENS = [GNO, COW, WETH, SAFE, WXDAI]
3236

3337

3438
# ABI
@@ -52,29 +56,28 @@ def _load_abi(abi_name: str) -> Dict:
5256

5357
# Variables
5458
START_BLOCK = int(os.environ.get("START_BLOCK", chain.blocks.head.number))
59+
HISTORICAL_BLOCK_STEP = int(os.environ.get("HISTORICAL_BLOCK_STEP", 720))
60+
EXTENSION_INTERVAL = int(os.environ.get("EXTENSION_INTERVAL", 6))
5561

5662

5763
# Local storage helper functions
58-
def _load_trades_db() -> Dict:
59-
"""
60-
Load trades database from CSV file or create new if doesn't exist.
61-
Returns dict with trade data indexed by block number.
62-
"""
64+
def _load_trades_db() -> pd.DataFrame:
65+
"""Load trades database from CSV file or create new if doesn't exist"""
6366
dtype = {
67+
"block_number": int,
6468
"owner": str,
6569
"sellToken": str,
6670
"buyToken": str,
67-
"sellAmount": object,
68-
"buyAmount": object,
69-
"block_number": np.int64,
71+
"sellAmount": str,
72+
"buyAmount": str,
7073
}
7174

7275
df = (
7376
pd.read_csv(TRADE_FILEPATH, dtype=dtype)
7477
if os.path.exists(TRADE_FILEPATH)
7578
else pd.DataFrame(columns=dtype.keys()).astype(dtype)
7679
)
77-
return df.to_dict("records")
80+
return df
7881

7982

8083
def _save_trades_db(trades_dict: Dict) -> None:
@@ -132,25 +135,43 @@ def _save_orders_db(df: pd.DataFrame) -> None:
132135

133136

134137
# Historical log helper functions
138+
def get_canonical_pair(token_a: str, token_b: str) -> tuple[str, str]:
139+
"""Return tokens in canonical order (alphabetically by address)"""
140+
return (token_a, token_b) if token_a.lower() < token_b.lower() else (token_b, token_a)
141+
142+
143+
def calculate_price(sell_amount: str, buy_amount: str) -> float:
144+
"""Calculate price from amounts"""
145+
return int(sell_amount) / int(buy_amount)
146+
147+
135148
def _process_trade_log(log) -> Dict:
136-
"""Process trade log and return formatted dictionary entry"""
149+
"""Process trade log with price calculation"""
150+
token_a, token_b = get_canonical_pair(log.sellToken, log.buyToken)
151+
price = calculate_price(log.sellAmount, log.buyAmount)
152+
153+
if token_a != log.sellToken:
154+
price = 1 / price
155+
137156
return {
138157
"block_number": log.block_number,
139158
"owner": log.owner,
140159
"sellToken": log.sellToken,
141160
"buyToken": log.buyToken,
142161
"sellAmount": str(log.sellAmount),
143162
"buyAmount": str(log.buyAmount),
163+
"token_a": token_a,
164+
"token_b": token_b,
165+
"price": price,
144166
}
145167

146168

147-
def _get_historical_gno_trades(
169+
def _get_historical_trades(
148170
settlement_contract,
149-
gno_address: str,
150171
start_block: int,
151172
stop_block: int = chain.blocks.head.number,
152173
):
153-
"""Get historical GNO trades from start_block to stop_block"""
174+
"""Get historical trades for monitored token pairs"""
154175
log_filter = LogFilter(
155176
addresses=[settlement_contract.address],
156177
events=[settlement_contract.Trade.abi],
@@ -159,23 +180,48 @@ def _get_historical_gno_trades(
159180
)
160181

161182
for log in accounts.provider.get_contract_logs(log_filter):
162-
if log.sellToken == gno_address or log.buyToken == gno_address:
183+
if log.sellToken in MONITORED_TOKENS and log.buyToken in MONITORED_TOKENS:
163184
yield log
164185

165186

166-
def _process_historical_gno_trades(
167-
settlement_contract, gno_address: str, start_block: int, stop_block: int
168-
) -> Dict:
169-
"""Process historical GNO trades and store in database"""
170-
trades_db = _load_trades_db()
187+
def _process_historical_trades(
188+
settlement_contract, start_block: int, stop_block: int
189+
) -> List[Dict]:
190+
"""Process historical trades and store in database"""
191+
trades = []
192+
193+
for log in _get_historical_trades(settlement_contract, start_block, stop_block):
194+
trades.append(_process_trade_log(log))
171195

172-
for log in _get_historical_gno_trades(
173-
settlement_contract, gno_address, start_block, stop_block
174-
):
175-
trades_db.append(_process_trade_log(log))
196+
if trades:
197+
existing_trades = _load_trades_db()
198+
all_trades = pd.concat([existing_trades, pd.DataFrame(trades)], ignore_index=True)
176199

177-
_save_trades_db(trades_db)
178-
return trades_db
200+
_save_trades_db(all_trades)
201+
202+
return trades
203+
204+
205+
def extend_historical_trades() -> None:
206+
"""Extend trades.csv data further back in history"""
207+
trades_df = _load_trades_db()
208+
209+
if len(trades_df) == 0:
210+
oldest_block = chain.blocks.head.number
211+
else:
212+
oldest_block = trades_df["block_number"].min()
213+
214+
new_trades = _process_historical_trades(
215+
GPV2_SETTLEMENT_CONTRACT,
216+
start_block=oldest_block - HISTORICAL_BLOCK_STEP,
217+
stop_block=oldest_block - 1,
218+
)
219+
220+
new_trades_df = pd.DataFrame(new_trades)
221+
all_trades = pd.concat([new_trades_df, trades_df])
222+
all_trades = all_trades.sort_values("block_number", ascending=True)
223+
224+
_save_trades_db(all_trades)
179225

180226

181227
# CoW Swap trading helper functions
@@ -313,30 +359,37 @@ def create_and_submit_order(
313359

314360
# Silverback bot
315361
@bot.on_startup()
316-
def app_startup(startup_state: StateSnapshot):
362+
def bot_startup(startup_state: StateSnapshot):
363+
"""Initialize bot state and historical data"""
317364
block_db = _load_block_db()
318365
last_processed_block = block_db["last_processed_block"]
319366

320-
_process_historical_gno_trades(
367+
_process_historical_trades(
321368
GPV2_SETTLEMENT_CONTRACT,
322-
GNO_ADDRESS,
323369
start_block=last_processed_block,
324370
stop_block=chain.blocks.head.number,
325371
)
326372

327373
_save_block_db({"last_processed_block": chain.blocks.head.number})
328-
374+
bot.state.last_extension_block = chain.blocks.head.number
329375
return {"message": "Starting...", "block_number": startup_state.last_block_seen}
330376

331377

332378
@bot.on_(chain.blocks)
333379
def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
334-
"""Execute block handler"""
335-
order_uid, error = create_and_submit_order(
336-
sell_token=GNO_ADDRESS, buy_token=COW_ADDRESS, sell_amount="20000000000000000000"
337-
)
338-
339-
if error:
340-
click.echo(f"Order failed: {error}")
341-
else:
342-
click.echo(f"Order submitted successfully. UID: {order_uid}")
380+
_save_block_db({"last_processed_block": block.number})
381+
382+
if block.number - bot.state.last_extension_block >= EXTENSION_INTERVAL:
383+
extend_historical_trades()
384+
bot.state.last_extension_block = block.number
385+
386+
387+
# """Execute block handler"""
388+
# order_uid, error = create_and_submit_order(
389+
# sell_token=GNO, buy_token=COW, sell_amount="20000000000000000000"
390+
# )
391+
#
392+
# if error:
393+
# click.echo(f"Order failed: {error}")
394+
# else:
395+
# click.echo(f"Order submitted successfully. UID: {order_uid}")

0 commit comments

Comments
 (0)