Backtesting fee analysis#
This is an example notebook how different fee models can be implemented in backtesting.
Some highlights of this notebook:
Uses synthetic data with a fixed asset price
Makes it easier to manual confirm correct price calculations
Show how to manually set up a fee model for a trading pair
Shows a fee calculation based on Uniswap v2 LP fee
Note that if you are running in this notebookin PyCharm you may encounter “IOPub data rate exceeded” error that needs a workaround.
Strategy logic and trade decisions#
We are interested in fees only: we do random sized buy and sell every midnight.
Trade 1h cycles, do one trade at every midnight, run for 2 months to generate a visualisation
Add some time between closed positions by checking when the last position was clsoed
[1]:
from typing import List, Dict
from tradingstrategy.universe import Universe
import pandas as pd
from tradeexecutor.state.trade import TradeExecution
from tradeexecutor.strategy.pricing_model import PricingModel
from tradeexecutor.strategy.pandas_trader.position_manager import PositionManager
from tradeexecutor.state.state import State
def decide_trades(
timestamp: pd.Timestamp,
universe: Universe,
state: State,
pricing_model: PricingModel,
cycle_debug_data: Dict) -> List[TradeExecution]:
pair = universe.pairs.get_single()
position_manager = PositionManager(timestamp, universe, state, pricing_model)
amount = random.choice([250, 500, 750, 1000])
trades = []
should_trade = False
if timestamp.hour == 0:
last_position = position_manager.get_last_closed_position()
if last_position:
# Check enough time has passed since the last trade
if timestamp - last_position.closed_at >= pd.Timedelta("4 days"):
should_trade = True
else:
should_trade = False
else:
should_trade = True # Open the first position
if should_trade:
if not position_manager.is_any_open():
# Buy
trades += position_manager.open_1x_long(pair, amount)
else:
# Sell
trades += position_manager.close_all()
return trades
Generating synthetic trading data#
We create a trading universe that has ETH/USD asset with a fixed $1000 price.
The pair has fixed 0.3% fee tier. We generate data for 8 weeks.
[2]:
import random
import datetime
from tradingstrategy.chain import ChainId
from tradingstrategy.timebucket import TimeBucket
from tradingstrategy.candle import GroupedCandleUniverse
from tradeexecutor.state.identifier import AssetIdentifier, TradingPairIdentifier
from tradeexecutor.testing.synthetic_ethereum_data import generate_random_ethereum_address
from tradeexecutor.testing.synthetic_exchange_data import generate_exchange
from tradeexecutor.testing.synthetic_price_data import generate_ohlcv_candles
from tradeexecutor.strategy.trading_strategy_universe import TradingStrategyUniverse, \
create_pair_universe_from_code
def create_trading_universe() -> TradingStrategyUniverse:
# Set up fake assets
mock_chain_id = ChainId.ethereum
mock_exchange = generate_exchange(
exchange_id=random.randint(1, 1000),
chain_id=mock_chain_id,
address=generate_random_ethereum_address())
usdc = AssetIdentifier(ChainId.ethereum.value, generate_random_ethereum_address(), "USDC", 6, 1)
weth = AssetIdentifier(ChainId.ethereum.value, generate_random_ethereum_address(), "WETH", 18, 2)
weth_usdc = TradingPairIdentifier(
weth,
usdc,
generate_random_ethereum_address(),
mock_exchange.address,
internal_id=random.randint(1, 1000),
internal_exchange_id=mock_exchange.exchange_id,
fee=0.003,
)
pair_universe = create_pair_universe_from_code(mock_chain_id, [weth_usdc])
candles = generate_ohlcv_candles(
TimeBucket.h1,
start=datetime.datetime(2021, 6, 1),
end=datetime.datetime(2021, 8, 1),
pair_id=weth_usdc.internal_id,
start_price=1000,
daily_drift=(1, 1),
high_drift=1,
low_drift=1,
)
candle_universe = GroupedCandleUniverse.create_from_single_pair_dataframe(candles)
universe = Universe(
time_bucket=TimeBucket.h1,
chains={mock_chain_id},
exchanges={mock_exchange},
pairs=pair_universe,
candles=candle_universe,
)
return TradingStrategyUniverse(universe=universe, reserve_assets=[usdc])
Examining the generated data#
Before starting the backtest, do a smoke check that our trading universe looks correct.
[3]:
universe = create_trading_universe()
start_at, end_at = universe.universe.candles.get_timestamp_range()
print(f"Our universe has synthetic data for the period {start_at} - {end_at}")
pair = universe.get_single_pair()
candles = universe.universe.candles.get_samples_by_pair(pair.internal_id)
min_price = candles["close"].min()
max_price = candles["close"].max()
print(f"We trade {pair}")
print(f"Price range is {min_price} - {max_price}")
Our universe has synthetic data for the period 2021-06-01 00:00:00 - 2021-07-31 23:00:00
We trade <Pair WETH-USDC spot_market_hold at 0x14B35C8d64B51D825F29afe4110B107b2e49f23a (0.3000% fee) on exchange 0xF07E677838492Cf23F7B12a55A036Cea292c441A>
Price range is 1000.0 - 1000.0
Running the backtest#
[4]:
from tradeexecutor.strategy.default_routing_options import TradeRouting
from tradeexecutor.strategy.cycle import CycleDuration
from tradeexecutor.strategy.reserve_currency import ReserveCurrency
from tradeexecutor.testing.synthetic_exchange_data import generate_simple_routing_model
from tradeexecutor.backtest.backtest_runner import run_backtest_inline
routing_model = generate_simple_routing_model(universe)
state, universe, debug_dump = run_backtest_inline(
name="Backtest fee calculation example",
start_at=start_at.to_pydatetime(),
end_at=end_at.to_pydatetime(),
client=None,
cycle_duration=CycleDuration.cycle_1h,
decide_trades=decide_trades,
universe=universe,
initial_deposit=10_000,
reserve_currency=ReserveCurrency.usdc,
trade_routing=TradeRouting.user_supplied_routing_model,
routing_model=routing_model,
)
Trading position chart#
We plot out a chart that shows - Our asset’s fixed price chart - Buys and sells around the fixed price that do not move the price - Mouse hover for any trade showing detailed price and fee analysis of this particular trade
[5]:
from tradeexecutor.visual.single_pair import visualise_single_pair
figure = visualise_single_pair(
state,
universe.universe.candles,
title="Position timeline with fee mouse hovers",
height=400,
)
figure.show()