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

from typing import List, Dict
from tradingstrategy.universe import Universe

import pandas as pd

from 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
                should_trade = False
            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)
            # 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.


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.

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 at 0xCcca9fB4cBd0b629E123572f511B9A5bf0765247 (0.3000% fee) on exchange 0xdF1AdED00f722f43A21FD068B8734e6b34b60c5B>
Price range is 1000.0 - 1000.0

Running the backtest#

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",

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

from tradeexecutor.visual.single_pair import visualise_single_pair

figure = visualise_single_pair(
    title="Position timeline with fee mouse hovers",