The Backtrader Fork That Took Apart Its Own Engine

cloudQuant/backtrader keeps the API traders already know, then rewrites the hot loops, adds tick-level simulation, and enforces a 900-line budget on the file everyone was afraid to touch.

9 min read • View on GitHub • More from cloudQuant

A cutaway view of a large clockwork engine mounted on a workbench. Its central housing, once a single cast block, has been divided into eight separate interlocking cassettes, each meshed with its neighbors and still turning; one cassette is lifted slightly out of its slot by a pair of pliers. The drawing stands for Cerebro, Backtrader's central orchestrator, split into eight mixins that still mesh as one public class.
Cerebro, Backtrader's orchestrator, split into eight cooperating mixins. The public class keeps its name and its exports; only the internal seams moved.
Key Takeaways

Backtrader has been the default answer to "how do I backtest a Python strategy" for nearly a decade. The upstream repository carries 22,831 stars. The fork from cloudQuant carries 97. That gap is the whole reason someone would fork a working library: upstream is stable, widely used, and largely done. The fork exists to move it forward anyway.

The most revealing change is not a new feature. It is a file that got smaller. Cerebro, the orchestrator object every Backtrader user touches, used to be one large class. In this fork it is a thin shell over eight mixins, each in its own file, and the module comment says the split was done "to keep the single public Cerebro class under the 900-line file budget."

A god object, disassembled

The class declaration reads like a table of responsibilities. Registry, notifications, run lifecycle, channel, execution, the two run loops, and presentation, each pulled out and imported back under a private alias.

class Cerebro(
    _RegistryMixin,
    _NotificationMixin,
    _RunLifecycleMixin,
    _ChannelMixin,
    _ExecutionMixin,
    _RunNextMixin,
    _RunOnceMixin,
    _PresentationMixin,
    ParameterizedBase,
):

The private aliases are the point. Because the mixins are imported as _ChannelMixin and friends rather than by their real names, from backtrader.cerebro import * still exports exactly what it exported before. The code comment cites "AC28-03 star-export parity" and notes that narrowing the imports "would be a breaking change." Someone measured this.

There is a cost, and the authors name it. lifecycle.py says its state "is created by Cerebro.__init__ on the assembled class; it cannot be seen from this partial class alone," and it disables pylint's no-member check to match. Reading any single method now means holding the full method resolution order in your head. That is the price of the line budget.


Two engines, one API

Backtrader has always had two execution paths, and this fork keeps both, optimizes both, and preserves the older variants for compatibility. runonce is the fast path. It sorts feeds by timeframe so the smallest one becomes the master clock, then peeks at each feed's next timestamp and advances only the feeds that are due. A feed that is exhausted reports float('inf'), so the loop ends when nothing delivers.

The trick that makes the vectorized path work is a reset. Indicators are computed in bulk with strat._once(), which writes into line buffers. Then strat.reset() rewinds those buffers so the per-bar replay sees a clean cursor. That is how indicators can be precomputed while strategies still see a sequential, bar-by-bar API.

The general path, runnext, runs every strategy's next() on each data arrival across four phases: notification, feed advance, time alignment, dispatch. The hot loop is full of small, deliberate specializations. For the common single-strategy case it caches bound methods to avoid attribute lookups. It builds a tuple of notify_idle handlers that actually override the base method, so the loop never calls a no-op.

The subtlest detail is the tri-state return from a feed: True means a bar arrived, False means the data is finished, and None means a live feed is connected but has no new bar yet. That None case still runs resamplers and notifications so a quiet live feed can tick without producing a bar. Backtest and live trading share one loop because that case exists.

How Cerebro is assembled, and the two loops that drive it.

What the speedup actually measures

The performance claim is specific and testable. The project reports that its dev branch ran a 1,271-strategy regression suite in 236.36 seconds against 438.96 seconds for master, a 1.86x speedup, with all 1,271 strategies passing on both branches. Those numbers are reported by the project, not reproduced by an outside party.

A second lever is native code. The fork ships a pybind11-accelerated wheel, back-trader-cpp, alongside the pure Python package, covering Python 3.8 through 3.14. The import name stays backtrader in both cases, so a strategy written for upstream runs unchanged.

From bars to ticks

Upstream Backtrader is bar-oriented. This fork adds tick events, order book snapshots, and funding events, plus an HFT simulation package under brokers/hft/ with a matching core, a queue model, a latency model, and a Binance best-bid-offer feed. The intended range runs from daily research down to tick-level simulation, in one codebase.

One codebase for backtest and live

The v1.2.0 release notes announce a unified live-trading path through bt_api_py, with a profiles module that builds a configured Cerebro and a channels package for streaming. A Bokeh web dashboard and a command-line runner round out the tooling. The same strategy object is meant to move from a research notebook to a live broker connection without a rewrite.

ProjectApproachExecution modesLive tradingInstall
cloudQuant/backtraderBacktrader fork, optimized enginerunonce + event-driven, tick and barUnified via bt_api_pyFrom source
mementum/backtrader (upstream)Original BacktraderEvent-driven, bar-orientedMultiple legacy integrationsPyPI
backtesting.pyLightweight bar backtestingBar-basedLimitedpip
vectorbtVectorized array researchVectorizedLimitedpip
QuantConnect/LeanC#-first platformEvent-drivenCloud platformPlatform
Zipline ReloadedEvent-driven researchEvent-drivenNonepip

The cost of moving fast

The fork is candid about its edges. It is not on PyPI, so every install is a source checkout. And v1.2.0 removed several legacy live-trading integrations outright.

This project is NOT on PyPI. Install from source only.

cloudQuant/backtrader maintainers, Project documentation · cloudQuant/backtrader

If you rely on any of these, do not upgrade and stay on the `development` branch.

cloudQuant, Release author · v1.2.0 release notes

That second line is the honest kind of release note. It tells you the upgrade will break you and points you at the branch that will not. For anyone already running Backtrader in production, the migration question is not whether the fork is faster. It is whether the broker integration you depend on survived.

So who is this for? Existing Backtrader users who want faster backtests, higher-frequency simulation, or a single codebase spanning research and live execution, and who are willing to install from source and track a fast-moving development branch. Everyone else is better served by the stability and the 22,831 stars of upstream, or by a lighter tool like backtesting.py.