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