Tags: fee-analysis, synthetic-data

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()