ML4T Backtest
ML4T Backtest Documentation
Event-driven backtesting with realistic execution
Skip to content

Run your first completed backtest

Install the package as described in Installation, then run the following Python script. It loads nine synthetic daily AAPL bars from the wheel, submits a buy order, submits a close order four asset bars later, and inspects the resulting fills, trade, and equity. The bundled data is described in Example Data.

from importlib.metadata import version

import polars as pl

from ml4t.backtest import BacktestConfig, DataFeed, Engine, Strategy
from ml4t.backtest.example_data import load_example_prices


class FirstRoundTrip(Strategy):
    def __init__(self):
        self.asset_bars = 0

    def on_data(self, timestamp, data, context, broker):
        if "AAPL" not in data:
            return
        self.asset_bars += 1
        if self.asset_bars == 1:
            broker.submit_order("AAPL", 10)
        elif self.asset_bars == 5:
            broker.close_position("AAPL")


prices = load_example_prices("equity").filter(pl.col("asset") == "AAPL")
feed = DataFeed(prices_df=prices)
engine = Engine(feed, FirstRoundTrip(), BacktestConfig(initial_cash=100_000))
result = engine.run()

print("ml4t-backtest " + version("ml4t-backtest"))
print(f"bars: {len(feed)}")
for fill in result.fills:
    print(f"{fill.timestamp:%Y-%m-%d} {fill.side.value} {fill.quantity:g} @ ${fill.price:.2f}")
print(f"closed trades: {len(result.to_trades_dataframe())}")
print(f"trade P&L: ${result.trades[0].pnl:.2f}")
equity = result.to_equity_dataframe()
print(f"equity points: {equity.height}")
print(f"final equity: ${result.metrics['final_value']:.2f}")
ml4t-backtest {package_version}
bars: 9
2024-01-03 buy 10 @ $188.37
2024-01-09 sell 10 @ $191.37
closed trades: 1
trade P&L: $30.00
equity points: 9
final equity: $100030.00

The feed provides timestamp, asset, and OHLCV columns. DataFeed delivers the AAPL bar to on_data at each timestamp. The first callback queues a buy; the fifth queues a close. The default NEXT_BAR execution mode fills both market orders at the next AAPL bar's open. The orders were submitted on January 2 and January 8, and the fills occurred on January 3 and January 9. The run starts with $100,000, charges no commission or slippage, and ends with one closed trade worth $30. The nine equity points include cash and the marked position after each bar. These synthetic prices illustrate accounting, not a claim about AAPL's historical return.

result.to_fills_dataframe() gives one row per execution. result.to_trades_dataframe() gives the completed position round trip, including entry, exit, and P&L. result.to_equity_dataframe() gives the dated account value. Inspect these three frames before interpreting a summary metric; Results & Analysis covers export and additional measures.

An Engine instance runs once. Create a new instance to test another strategy or setting. Continue with Order Types and Execution Semantics to see how order choice and timing change fills.

In the book

Chapter 16, Section 16.3, Vectorized and event-driven backtesting, and notebook 04, Single Asset Backtest with ml4t-backtest extend this first round trip to a stateful RSI rule, explicit costs, and a matched comparison with a vectorized backtest.