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