How to Backtest ML-Driven Trading Strategies Using Zipline

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, 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:

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

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, which supports scikit-learn, TensorFlow, or PyTorch models:

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:

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:

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:

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:

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 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.

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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →