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.
- The fork splits Cerebro, Backtrader's central god object, into eight mixins to fit an enforced 900-line file budget without changing a single public name.
- Two execution engines coexist: a vectorized runonce path that computes indicators in bulk and an event-driven runnext path that can tick on a live feed returning no new bar.
- The project reports a 1.86x speedup on its 1,271-strategy regression suite, a self-reported benchmark rather than an independently reproduced one.
- It pushes upstream Backtrader into tick, order book, and HFT territory, but installs from source only and removed legacy live integrations in v1.2.0.
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.
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.
| Project | Approach | Execution modes | Live trading | Install |
|---|---|---|---|---|
| cloudQuant/backtrader | Backtrader fork, optimized engine | runonce + event-driven, tick and bar | Unified via bt_api_py | From source |
| mementum/backtrader (upstream) | Original Backtrader | Event-driven, bar-oriented | Multiple legacy integrations | PyPI |
| backtesting.py | Lightweight bar backtesting | Bar-based | Limited | pip |
| vectorbt | Vectorized array research | Vectorized | Limited | pip |
| QuantConnect/Lean | C#-first platform | Event-driven | Cloud platform | Platform |
| Zipline Reloaded | Event-driven research | Event-driven | None | pip |
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.
If you rely on any of these, do not upgrade and stay on the `development` branch.
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.





