Backtest (low-level API)
Tutorial for PoseiTrader a high-performance algorithmic trading platform and event driven backtester.
Overview
This tutorial walks through how to use a
BacktestEngine
to backtest a simple
EMA cross strategy with a TWAP execution algorithm
on a simulated Binance Spot exchange using
historical trade tick data.
The following points will be covered:
- How to load raw data (external to Posei) using data loaders and wranglers
-
How to add this data to a
BacktestEngine
-
How to add venues, strategies and execution
algorithms to a
BacktestEngine
-
How to run backtests with a
BacktestEngine
- Post-run analysis and options for repeated runs
Prerequisites
- Python 3.11+ installed
-
JupyterLab
or similar installed (
pip install -U jupyterlab
) -
PoseiTrader
latest release installed (
pip install -U posei_trader
)
Imports
We'll start with all of our imports for the remainder of this tutorial.
from decimal import Decimal
from posei_trader.backtest.engine import BacktestEngine
from posei_trader.backtest.engine import BacktestEngineConfig
from posei_trader.examples.algorithms.twap import TWAPExecAlgorithm
from posei_trader.examples.strategies.ema_cross_twap import EMACrossTWAP
from posei_trader.examples.strategies.ema_cross_twap import EMACrossTWAPConfig
from posei_trader.model import BarType
from posei_trader.model import Money
from posei_trader.model import TraderId
from posei_trader.model import Venue
from posei_trader.model.currencies import ETH
from posei_trader.model.currencies import USDT
from posei_trader.model.enums import AccountType
from posei_trader.model.enums import OmsType
from posei_trader.persistence.wranglers import TradeTickDataWrangler
from posei_trader.test_kit.providers import TestDataProvider
from posei_trader.test_kit.providers import TestInstrumentProvider
Loading data
For this tutorial we'll use some stub test data which exists in the PoseiTrader repository (this data is also used by the automated test suite to test the correctness of the platform).
Firstly, instantiate a data provider which we can
use to read raw CSV trade tick data into memory as
a pd.DataFrame
. We then need to
initialize the instrument which matches the data,
in this case the ETHUSDT
spot
cryptocurrency pair for Binance. We'll use
this instrument for the remainder of this backtest
run.
Next, we need to wrangle this data into a list of
Posei TradeTick
objects, which can we
later add to the BacktestEngine
.
# Load stub test data
provider = TestDataProvider()
trades_df = provider.read_csv_ticks("binance/ethusdt-trades.csv")
# Initialize the instrument which matches the data
ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance()
# Process into Posei objects
wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE)
ticks = wrangler.process(trades_df)
See the Loading External Data guide for a more detailed explanation of the typical data processing components and pipeline.
Initialize a backtest engine
Now we'll need a backtest engine, minimally
you could just call
BacktestEngine()
which will
instantiate an engine with a default
configuration.
Here we also show initializing a
BacktestEngineConfig
(will only a
custom trader_id
specified) to show
the general configuration pattern.
See the Configuration API reference for details of all configuration options available.
# Configure backtest engine
config = BacktestEngineConfig(trader_id=TraderId("BACKTESTER-001"))
# Build the backtest engine
engine = BacktestEngine(config=config)
Add venues
We'll need a venue to trade on, which should match the market data being added to the engine.
In this case we'll set up a simulated Binance Spot exchange.
# Add a trading venue (multiple venues possible)
BINANCE = Venue("BINANCE")
engine.add_venue(
venue=BINANCE,
oms_type=OmsType.NETTING,
account_type=AccountType.CASH, # Spot CASH account (not for perpetuals or futures)
base_currency=None, # Multi-currency account
starting_balances=[Money(1_000_000.0, USDT), Money(10.0, ETH)],
)
Add data
Now we can add data to the backtest engine. First
add the Instrument
object we
previously initialized, which matches our data.
Then we can add the trades we wrangled earlier.
# Add instrument(s)
engine.add_instrument(ETHUSDT_BINANCE)
# Add data
engine.add_data(ticks)
The amount of and variety of data types is only limited by machine resources and your imagination (custom types are possible). Also, multiple venues can be used for backtesting, again only limited by machine resources.
Add strategies
Now we can add the trading strategies we’d like to run as part of our system.
Multiple strategies and instruments can be used for backtesting, only limited by machine resources.
Firstly, initialize a strategy configuration, then use this to initialize a strategy which we can add to the engine:
# Configure your strategy
strategy_config = EMACrossTWAPConfig(
instrument_id=ETHUSDT_BINANCE.id,
bar_type=BarType.from_str("ETHUSDT.BINANCE-250-TICK-LAST-INTERNAL"),
trade_size=Decimal("0.10"),
fast_ema_period=10,
slow_ema_period=20,
twap_horizon_secs=10.0,
twap_interval_secs=2.5,
)
# Instantiate and add your strategy
strategy = EMACrossTWAP(config=strategy_config)
engine.add_strategy(strategy=strategy)
You may notice that this strategy config includes
parameters related to a TWAP execution algorithm.
This is because we can flexibly use different
parameters per order submit, we still need to
initialize and add the actual
ExecAlgorithm
component which will
execute the algorithm - which we'll do now.
Add execution algorithms
PoseiTrader enables us to build up very complex systems of custom components. Here we show just one of the custom components available, in this case a built-in TWAP execution algorithm. It is configured and added to the engine in generally the same pattern as for strategies:
Multiple execution algorithms can be used for backtesting, only limited by machine resources.
# Instantiate and add your execution algorithm
exec_algorithm = TWAPExecAlgorithm() # Using defaults
engine.add_exec_algorithm(exec_algorithm)
Run backtest
Now that we have our data, venues and trading
system configured - we can run a backtest Simply
call the .run(...)
method which will
run a backtest over all available data by default.
See the BacktestEngineConfig API reference for a complete description of all available methods and options.
# Run the engine (from start to end of data)
engine.run()
Post-run and analysis
Once the backtest is completed, a post-run tearsheet will be automatically logged using some default statistics (or custom statistics which can be loaded, see the advanced Portfolio statistics guide).
Also, many resultant data and execution objects will be held in memory, which we can use to further analyze the performance by generating various reports.
engine.trader.generate_account_report(BINANCE)
engine.trader.generate_order_fills_report()
engine.trader.generate_positions_report()
Repeated runs
We can also choose to reset the engine for
repeated runs with different strategy and
component configurations. Calling the
.reset(...)
method will retain all
loaded data and components, but reset all other
stateful values as if we had a fresh
BacktestEngine
(this avoids having to
load the same data again).
# For repeated backtest runs make sure to reset the engine
engine.reset()
Individual components (actors, strategies, execution algorithms) need to be removed and added as required.
See the Trader API reference for a description of all methods available to achieve this.
# Once done, good practice to dispose of the object if the script continues
engine.dispose()