# How to Backtest ML-Driven Trading Strategies Using Zipline

> Learn to backtest ML-driven trading strategies using Zipline. Register custom data, wrap your model, and convert predictions to portfolio allocations for robust testing.

- Repository: [Stefan Jansen/machine-learning-for-trading](https://github.com/stefan-jansen/machine-learning-for-trading)
- Tags: how-to-guide
- Published: 2026-06-02

---

**You can backtest ML-driven trading strategies with Zipline by registering a custom data bundle that ingests minute-level market data, wrapping your trained model in an inference function, and implementing the `initialize` and `handle_data` callbacks to convert predictions into portfolio allocations.**

The **stefan-jansen/machine-learning-for-trading** repository provides a production-ready framework to backtest ML-driven trading strategies using Zipline. This workflow bridges the gap between model training and live simulation by converting raw Algoseek minute-bar data into Zipline-compatible bundles and embedding machine learning predictions directly into the trading logic.

## Register a Custom Data Bundle and Trading Calendar

Zipline requires a **data bundle**—a collection of OHLCV bars plus an exchange calendar that defines valid trading sessions. In [`08_ml4t_workflow/04_ml4t_workflow_with_zipline/01_custom_bundles/extension.py`](https://github.com/stefan-jansen/machine-learning-for-trading/blob/main/08_ml4t_workflow/04_ml4t_workflow_with_zipline/01_custom_bundles/extension.py), the repository registers an `algoseek` bundle with a custom `AlgoSeekCalendar` that extends `XNYSExchangeCalendar`.

The calendar defines extended hours from 4:00 AM to 7:59 PM Eastern Time, supporting 960 minutes per day for pre-market and post-market analysis:

```python
from trading_calendars import register_calendar, XNYSExchangeCalendar
from zipline.data.bundles import register
from datetime import time
from pytz import timezone

class AlgoSeekCalendar(XNYSExchangeCalendar):
    @property
    def name(self):
        return "AlgoSeek"

    @property
    def tz(self):
        return timezone("US/Eastern")

    open_times = ((None, time(4, 1)),)
    close_times = ((None, time(19, 59)),)

register_calendar('AlgoSeek', AlgoSeekCalendar())

register(
    'algoseek',
    algoseek_to_bundle(),  # Custom ingestion function

    calendar_name='AlgoSeek',
    minutes_per_day=960,
)

```

After defining the extension, ingest the data once (or whenever raw data updates):

```bash
zipline ingest -b algoseek

```

This populates `~/.zipline/data/algoseek/` with minute-level bars ready for backtesting.

## Load and Wrap Your ML Model for Inference

To integrate a trained classifier or regressor, create a lightweight wrapper that loads the model once and exposes a `predict` function. The repository demonstrates this pattern in [`08_ml4t_workflow/04_ml4t_workflow_with_zipline/01_custom_bundles/algoseek_1min_trades.py`](https://github.com/stefan-jansen/machine-learning-for-trading/blob/main/08_ml4t_workflow/04_ml4t_workflow_with_zipline/01_custom_bundles/algoseek_1min_trades.py), which supports scikit-learn, TensorFlow, or PyTorch models:

```python
import joblib
import pathlib

def load_model():
    model_path = pathlib.Path('models/my_cnn.pkl')
    return joblib.load(model_path)

def predict(model, features):
    """
    Return a scalar signal: positive values indicate long positions,
    negative values indicate short positions.
    """
    return model.predict(features.reshape(1, -1))[0]

```

The `features` vector typically consists of the current bar’s OHLCV values, technical indicators, or embeddings generated from prior pipeline steps.

## Implement the Zipline Algorithm

The trading logic resides in two required callbacks: `initialize` (runs once at start) and `handle_data` (runs every minute). The following implementation loads the model during initialization, constructs feature vectors from real-time bars, and maps model outputs to target portfolio weights using `order_target_percent`:

```python
import pandas as pd
from zipline.api import (
    order_target_percent, record, symbol, set_benchmark
)
from algoseek_1min_trades import load_model, predict

def initialize(context):
    # Load model once to avoid I/O overhead per bar

    context.model = load_model()
    context.asset = symbol('AAPL')
    set_benchmark(context.asset)

def handle_data(context, data):
    # Extract latest minute bar

    bar = data.current(
        context.asset, 
        ['open', 'high', 'low', 'close', 'volume']
    )
    features = pd.Series(bar).values.astype(float)
    
    # Generate ML signal and convert to position size [-1.0, 1.0]

    signal = predict(context.model, features)
    target_weight = max(min(signal, 1.0), -1.0)
    
    # Execute trade

    order_target_percent(context.asset, target_weight)
    
    # Record for tear sheet analysis

    record(
        price=bar['close'], 
        signal=signal, 
        weight=target_weight
    )

```

## Execute the Backtest

You can run the algorithm via command line or the Python API. The CLI method is useful for batch execution:

```bash
zipline run -b algoseek -s 2019-01-01 -e 2020-12-31 \
    -f backtest_algorithm.py \
    -o results.pkl \
    --capital-base 100000

```

For tighter integration with Jupyter notebooks and model training pipelines, use the Python API as demonstrated in `17_deep_learning/05_backtesting_with_zipline.ipynb`:

```python
from zipline import run_algorithm
from datetime import datetime
from pytz import UTC

perf = run_algorithm(
    start=datetime(2019, 1, 1, tzinfo=UTC),
    end=datetime(2020, 12, 31, tzinfo=UTC),
    initialize=initialize,
    handle_data=handle_data,
    capital_base=100_000,
    bundle='algoseek',
)

perf.to_pickle('ml_backtest_results.pkl')

```

## Evaluate Results with Pyfolio and Alphalens

After generating the performance DataFrame, analyze risk-adjusted returns and factor efficacy. The repository utilizes **Pyfolio** for tear sheets and **Alphalens** to evaluate the predictive power of the ML signal:

```python
import pyfolio as pf
import alphalens as al

# Load results

perf = pd.read_pickle('ml_backtest_results.pkl')

# Pyfolio tear sheet

returns = pf.utils.get_returns(perf)
pf.create_full_tear_sheet(returns)

# Alphalens factor analysis

factor = al.utils.get_clean_factor_and_forward_returns(
    perf['signal'],
    perf['price'],
    quantiles=5,
    max_loss=0.35
)
al.tears.create_summary_tear_sheet(factor)

```

## Summary

- **Custom bundles** are required to ingest non-standard minute data; register them in [`extension.py`](https://github.com/stefan-jansen/machine-learning-for-trading/blob/main/extension.py) with a compatible trading calendar.
- **Model wrappers** should expose a consistent `predict` interface to keep algorithm code framework-agnostic.
- **Zipline callbacks** (`initialize` and `handle_data`) separate setup logic from per-bar execution, enabling efficient vectorized operations.
- **Position sizing** uses `order_target_percent` to translate continuous model outputs into discrete portfolio weights between -1.0 and 1.0.
- **Performance analytics** combine Pyfolio for portfolio statistics and Alphalens for signal quality assessment.

## Frequently Asked Questions

### What data format does Zipline require for ML backtesting?

Zipline expects a **bundle** containing OHLCV data indexed by minute or day timestamps, paired with a registered trading calendar that defines market open and close times. The repository demonstrates this using Algoseek minute-bar data converted via a custom `algoseek_to_bundle` function in [`extension.py`](https://github.com/stefan-jansen/machine-learning-for-trading/blob/main/extension.py).

### Can I use deep learning models like TensorFlow or PyTorch with Zipline?

Yes. The `load_model` and `predict` wrapper functions in [`algoseek_1min_trades.py`](https://github.com/stefan-jansen/machine-learning-for-trading/blob/main/algoseek_1min_trades.py) are framework-agnostic. You can load TensorFlow SavedModels or PyTorch state dictionaries inside `initialize` and call `.predict()` or forward passes within `handle_data`, provided the model serialization format is compatible with Python's standard libraries.

### How do I handle custom trading hours in Zipline?

Subclass `XNYSExchangeCalendar` (or another base calendar) and override the `open_times` and `close_times` properties, then register the custom calendar with `register_calendar()`. The repository’s `AlgoSeekCalendar` implements 4:00 AM to 7:59 PM Eastern Time to capture extended trading sessions.

### What is the difference between using the CLI and Python API for Zipline backtests?

The **CLI** (`zipline run`) is optimal for scheduled batch jobs and configuration-driven workflows, saving results directly to disk. The **Python API** (`run_algorithm`) returns a performance DataFrame in-memory, making it ideal for interactive research in Jupyter notebooks where you need to iterate rapidly between model training and backtesting, as shown in `05_backtesting_with_zipline.ipynb`.