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

Trade execution chart#

Plot a chart to examine the success of trade execution.

  • This is for example purpose only.

  • The trade execution chart is meaningful in this backtesting notebook, because the backtesting assumes perfect execution and there is no slippage or price impact.

  • This chart is more meaningful for analysing logs of live execution to see how much slippage and price impact increased fees

[6]:
from tradeexecutor.visual.single_pair import visualise_single_pair_positions_with_duration_and_slippage

fig = visualise_single_pair_positions_with_duration_and_slippage(
    state,
    candles,
    title="Execution success chart",
    height=400,
)

display(fig)

Strategy summary#

Overview of strategy performance.

We manually check that fees were correctly calculated.

[7]:
from tradeexecutor.analysis.trade_analyser import build_trade_analysis

analysis = build_trade_analysis(state.portfolio)
summary = analysis.calculate_summary_statistics(TimeBucket.h1, state)
summary.display()
Returns
Annualised return % -2.64%
Lifetime return % -0.43%
Realised PnL $-41.87
Trade period 60 days 0 hours
Holdings
Total assets $9,956.63
Cash left $9,708.13
Open position value $248.50
Open positions 1
Winning Losing Total
Closed Positions
Number of positions 0 12 12
% of total 0.00% 100.00% 100.00%
Average PnL % 0.00% -0.60% -0.60%
Median PnL % - -0.60% -0.60%
Biggest PnL % - -0.60% -
Average duration 0 bars 24 bars 24 bars
Max consecutive streak 0 12 -
Max runup / drawdown 0.00% -0.40% -
Stop losses Take profits
Position Exits
Triggered exits 0 0
Percent winning - -
Percent losing - -
Percent of total 0.00% 0.00%
Risk Analysis
Biggest realized risk 10.03%
Average realized risk -0.03%
Max pullback of capital -0.42%
Sharpe Ratio -812.23%
Sortino Ratio -752.73%
Profit Factor 0.00%

Trading position timeline#

Display all positions and how much profit they made. Manually check the total swap fees column that it looks correct.

[8]:
from IPython.core.display_functions import display

from tradeexecutor.analysis.trade_analyser import expand_timeline

timeline = analysis.create_timeline()

expanded_timeline, apply_styles = expand_timeline(
        universe.universe.exchanges,
        universe.universe.pairs,
        timeline)

# Do not truncate the row output
with pd.option_context("display.max_row", None):
    display(apply_styles(expanded_timeline))

Remarks Type Opened at Duration Exchange Base asset Quote asset Position max value PnL USD PnL % Open mid price USD Close mid price USD Trade count LP fees
Long 2021-06-01 1 days WETH USDC $500.00 $-2.99 -0.60% $1,003.000000 $997.000000 2 $3.00
Long 2021-06-06 1 days WETH USDC $750.00 $-4.49 -0.60% $1,003.000000 $997.000000 2 $4.49
Long 2021-06-11 1 days WETH USDC $250.00 $-1.50 -0.60% $1,003.000000 $997.000000 2 $1.50
Long 2021-06-16 1 days WETH USDC $750.00 $-4.49 -0.60% $1,003.000000 $997.000000 2 $4.49
Long 2021-06-21 1 days WETH USDC $1,000.00 $-5.98 -0.60% $1,003.000000 $997.000000 2 $5.99
Long 2021-06-26 1 days WETH USDC $750.00 $-4.49 -0.60% $1,003.000000 $997.000000 2 $4.49
Long 2021-07-01 1 days WETH USDC $250.00 $-1.50 -0.60% $1,003.000000 $997.000000 2 $1.50
Long 2021-07-06 1 days WETH USDC $250.00 $-1.50 -0.60% $1,003.000000 $997.000000 2 $1.50
Long 2021-07-11 1 days WETH USDC $250.00 $-1.50 -0.60% $1,003.000000 $997.000000 2 $1.50
Long 2021-07-16 1 days WETH USDC $1,000.00 $-5.98 -0.60% $1,003.000000 $997.000000 2 $5.99
Long 2021-07-21 1 days WETH USDC $500.00 $-2.99 -0.60% $1,003.000000 $997.000000 2 $3.00
Long 2021-07-26 1 days WETH USDC $750.00 $-4.49 -0.60% $1,003.000000 $997.000000 2 $4.49
Long 2021-07-31 WETH USDC $250.00 $1,003.000000 1 $0.75

Finishing notes#

Print out a line to signal the notebook finished the execution successfully.

[9]:
print("All ok")
All ok