High-performance C++20 backtesting engine with Python interface
Project description
QuantCore
High-performance backtesting engine for trading strategies, written in C++20 with a Python research interface.
Overview
QuantCore is an event-driven backtester built around an enhanced version of my limit order book simulator. It processes market events chronologically through a priority queue, ensuring no look-ahead bias and no unrealistic assumptions about fill prices.
The C++ core handles all the performance-critical work: event dispatch, order matching, position tracking, and execution simulation. Python sits on top via pybind11 bindings and handles strategy development, parameter optimization, and visualization.
Market Data → EventQueue → Strategy → Signal → OrderBook → Fill → Portfolio
Both bar data and tick data are supported. The engine is bar-agnostic internally - ticks become
MarketDataEvent objects like bars do, so strategies work unchanged across both data types.
Quick Start
Bar data
# pip install quantcore
import quantcore as qc
class MyStrategy(qc.Strategy):
def on_data(self, event):
if not self.has_position(event.symbol):
self.generate_signal(event.symbol, qc.SignalType.BUY, 1.0, event.timestamp_ns)
results = qc.run_backtest(
strategy=MyStrategy(),
data={'AAPL': qc.load_csv_data('data/aapl.csv', 'AAPL')},
initial_capital=100_000.0,
)
print(results)
Tick data
results = qc.run_tick_backtest(
strategy=MyStrategy(),
tick_data={'AAPL': qc.load_tick_csv('data/aapl_ticks.csv', 'AAPL')},
initial_capital=100_000.0,
mm_refresh_interval_ns=1_000_000_000, # refresh MM quotes once per second
equity_snapshot_interval_ns=60_000_000_000, # snapshot equity once per minute
)
print(results)
The same strategy class works for both. event.close is the tick price when running on tick data.
You can also aggregate ticks to bars before running:
ticks = qc.load_tick_csv('data/aapl_ticks.csv', 'AAPL')
bars = qc.aggregate_ticks_to_bars(ticks, bar_duration_ns=60_000_000_000) # 1-minute bars
results = qc.run_backtest(strategy=MyStrategy(), data={'AAPL': bars}, initial_capital=100_000.0)
The engine handles fills, position tracking, and PnL automatically. For a full tearsheet:
from quantcore.analytics import calculate_all_metrics, calculate_returns
from quantcore.plotting import plot_full_tearsheet
import numpy as np
equity = np.array(results['equity_curve'])
returns = calculate_returns(equity)
print(calculate_all_metrics(equity))
plot_full_tearsheet(equity, returns)
For the full API reference and usage guide, see docs/usage.md.
Architecture
Every action goes through the event queue. When a strategy calls generate_signal, that signal becomes an OrderEvent, which goes through the order book, produces a FillEvent, which updates the portfolio. All in timestamp order. This is what prevents look-ahead bias: the strategy never sees data from the future.
Strategy Development
Python Strategy
Subclass qc.Strategy and implement on_data. The same strategy works for bar and tick data - event.close is the close price for bars and the trade price for ticks.
class BollingerBreakout(qc.Strategy):
def __init__(self, window=20, n_std=2.0):
super().__init__("BollingerBreakout")
self.window = window
self.n_std = n_std
self.prices = []
def on_data(self, event):
self.prices.append(event.close)
if len(self.prices) < self.window:
return
window_prices = self.prices[-self.window:]
mean = sum(window_prices) / self.window
std = (sum((p - mean) ** 2 for p in window_prices) / self.window) ** 0.5
upper = mean + self.n_std * std
lower = mean - self.n_std * std
pos = self.get_position(event.symbol)
if event.close > upper and pos <= 0:
self.generate_signal(event.symbol, qc.SignalType.BUY, 1.0, event.timestamp_ns)
elif event.close < lower and pos >= 0:
self.generate_signal(event.symbol, qc.SignalType.SELL, 1.0, event.timestamp_ns)
def on_fill(self, fill):
pass # optional: react to fills
Portfolio Context
Strategies can access full portfolio state:
def on_data(self, event):
portfolio = self.get_portfolio()
if portfolio:
equity = portfolio.get_portfolio_value()
cash = portfolio.get_cash()
position = portfolio.get_position(event.symbol)
Position Sizing
The engine ships with several sizing methods:
from quantcore import FixedPercentage, RiskBased, KellyCriterion
engine = qc.BacktestEngine(100_000.0)
engine.set_position_sizer(qc.FixedPercentage(0.10)) # 10% of capital per trade
Built-in sizers: FixedPercentage, RiskBased, KellyCriterion, EqualWeight, VolatilityTargeting, FixedShares.
Tick Data
Loading
# from CSV - columns: timestamp, price, quantity (or with side: B/S/buy/sell)
ticks = qc.load_tick_csv('data/aapl_ticks.csv', 'AAPL')
# from Parquet
ticks = qc.load_tick_parquet('data/aapl_ticks.parquet', 'AAPL')
# numpy fast path - (N, 4) array: [timestamp_ns, price, quantity, side]
# side: 0.0 = Buy, 1.0 = Sell
arr = qc.load_tick_parquet('data/aapl_ticks.parquet', use_numpy=True)
engine.add_tick_data('AAPL', arr)
Aggregation
# aggregate to any bar duration
bars_1min = qc.aggregate_ticks_to_bars(ticks, bar_duration_ns=60_000_000_000)
bars_1hour = qc.aggregate_ticks_to_bars(ticks, bar_duration_ns=3_600_000_000_000)
bars_1day = qc.aggregate_ticks_to_bars(ticks, bar_duration_ns=86_400_000_000_000)
Engine configuration for tick data
Two settings matter most for tick performance:
engine = qc.BacktestEngine(100_000.0)
engine.add_tick_data('AAPL', ticks)
# How often the market maker refreshes its quotes.
# 0 = every tick (default, slowest). 1s interval gives ~5x speedup on 1-second tick data.
engine.set_mm_refresh_interval(1_000_000_000) # 1 second
# How often the equity curve is snapshotted.
# 0 = every tick (default). Has negligible performance impact but keeps the
# equity curve manageable for large tick datasets.
engine.set_equity_snapshot_interval(60_000_000_000) # 1 minute
run_tick_backtest() sets both to sensible defaults (1s and 60s respectively).
CSV format
timestamp,price,quantity
1700000000,150.25,100
1700000001,150.30,50
# with aggressor side
timestamp,price,quantity,side
1700000000,150.25,100,buy
1700000001,150.30,50,sell
Timestamps in seconds, milliseconds, microseconds, or nanoseconds - detected automatically.
Execution Simulation
Order Types
GOOD_TILL_CANCEL, IMMEDIATE_OR_CANCEL, FILL_OR_KILL, MARKET, GOOD_FOR_DAY
Fees & Slippage
from quantcore import ExecutionConfig
config = ExecutionConfig()
config.maker_fee = 0.001 # 0.1% maker
config.taker_fee = 0.002 # 0.2% taker
config.slippage_pct = 0.0005 # 0.05% slippage
config.latency_ns = 1_000_000 # 1ms order latency
engine = qc.BacktestEngine(100_000.0, config)
Risk Management
from quantcore import RiskLimits
limits = qc.RiskLimits()
limits.max_position_pct = 0.20 # max 20% per position
limits.max_leverage = 2.0
limits.max_loss_pct = 0.15 # halt at 15% drawdown
engine.set_risk_limits(limits)
Performance
Single-threaded. Measured on Windows (Release build, MSVC). Full results in benchmarks/RESULTS.md.
Order book
| Pattern | Ops/s |
|---|---|
| Add + cancel (market-maker quote refresh) | 13.0 M ops/s |
| Add + match (taker sweep) | 4.9 M ops/s |
End-to-end backtest - bar mode
| Scenario | Bars/s | Latency (p99) |
|---|---|---|
| 1-year (252 bars) | ~270 K bars/s | 0.93 ms |
| 5-year (1,260 bars) | ~270 K bars/s | - |
| 1,000-year stress (252,000 bars) | ~290 K bars/s | - |
End-to-end backtest - tick mode
| Scenario | Ticks/s | Wall time |
|---|---|---|
| 10K ticks, no MM throttle | 315 K/s | 31.7 ms |
| 10K ticks, 1s MM throttle | 1.66 M/s | 6.0 ms |
| 10K ticks, 10s MM throttle | 2.78 M/s | 3.6 ms |
| 1M ticks → 1-min bars (aggregation) | 247 M/s | 4.1 ms |
The market-maker refresh interval is the main performance lever for tick data. With a 1-second
throttle the engine processes 1-second tick data at ~5x the unthrottled rate. See
benchmarks/RESULTS.md for the full breakdown.
Run the benchmarks yourself:
cmake --build build --target bench_backtest_engine bench_tick_data
./build/bench_backtest_engine
./build/bench_tick_data
python benchmarks/bench_python.py
python benchmarks/bench_tick_python.py
Analytics
After running a backtest, the results dict contains an equity curve and trade log you can feed straight into the analytics module.
from quantcore.analytics import calculate_all_metrics, calculate_returns
equity = np.array(results['equity_curve'])
returns = calculate_returns(equity)
metrics = calculate_all_metrics(equity)
print(metrics)
# Total Return: 24.31%
# Annualized: 11.82%
# Sharpe Ratio: 1.43
# Sortino Ratio: 2.01
# Max Drawdown: -8.74%
# Win Rate: 58.3%
Available metrics: total return, CAGR, Sharpe, Sortino, Calmar, max drawdown, drawdown duration, win rate, profit factor, avg win/loss, largest win/loss.
Visualizations
from quantcore.plotting import (
plot_full_tearsheet,
plot_equity_curve,
plot_underwater,
plot_returns_distribution,
plot_rolling_metrics,
plot_monthly_returns_heatmap,
)
plot_full_tearsheet(equity, returns, timestamps=ts)
Example Notebooks
| Notebook | Strategy | Concepts |
|---|---|---|
mean_reversion.ipynb |
Z-score mean reversion | Parameter sensitivity, OU process |
sma_crossover.ipynb |
SMA crossover | Trend following, signal generation |
pairs_trading.ipynb |
Statistical arbitrage | Cointegration, spread trading |
build_your_own_strategy.ipynb |
Bollinger Band Breakout | Full walkthrough from scratch |
Installation
Prerequisites
- CMake 3.15+
- C++20 compiler (GCC 10+, Clang 12+, MSVC 2022)
- Python 3.8+
- pybind11 (
pip install pybind11)
Build
git clone https://github.com/SLMolenaar/quantcore.git
cd quantcore
# build the C++ core
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# build the Python bindings
cd python
pip install pybind11
python build_module.py
# verify
python -c "import quantcore; print(quantcore.version())"
Run Tests
cmake --build build --target quantcore_tests
./build/quantcore_tests
For the full Python API reference, see docs/usage.md.
Project Structure
quantcore/
├── cpp/
│ ├── backtesting/ # Engine, events, portfolio
│ │ ├── tick_data.h # TickData struct and TickSeries
│ │ └── tick_data_loader.h# CSV loader and aggregate_to_bars
│ ├── strategies/ # C++ strategy implementations
│ ├── orderbook/ # Order book (from orderbook-simulator-cpp)
│ └── tests/ # GoogleTest suite
├── python/
│ ├── quantcore/ # Python package
│ │ ├── __init__.py # Public API
│ │ ├── analytics.py # Performance metrics
│ │ ├── plotting.py # Visualizations
│ │ └── tick_parquet_loader.py # Parquet loader for tick data
│ ├── bindings.cpp # pybind11 bindings
│ └── build_module.py # Build helper
├── examples/ # Jupyter notebooks
├── benchmarks/ # Benchmark suite
│ ├── bench_backtest_engine.cpp
│ ├── bench_tick_data.cpp
│ ├── bench_python.py
│ ├── bench_tick_python.py
│ └── RESULTS.md
├── CMakeLists.txt
└── README.md
vs. Alternatives
| QuantCore | Backtrader | Zipline | |
|---|---|---|---|
| Core language | C++20 | Python | Python |
| Order book simulation | ✅ Real LOB | ❌ | ❌ |
| Tick data support | ✅ Native | ❌ | ❌ |
| Event-driven | ✅ | ✅ | ✅ |
| Look-ahead prevention | ✅ Priority queue | ✅ | ✅ |
| Python strategy API | ✅ pybind11 | ✅ native | ✅ native |
| Throughput (bars/s) | ~300K | ~7.7K | unverified |
| Maintenance | Active | Stale | Inactive |
Throughput measured on SMA(50/200) crossover, 50K daily bars, Release build, Windows.
Reproduce: python benchmarks/qcVsBacktrader.py --bars 50000 --runs 20
The main differentiator is the order book. Backtrader and Zipline assume you fill at the bar's close price. QuantCore routes orders through a real price-time priority matching engine, which gives you realistic partial fills, spread simulation, and tick-level execution when you have tick data.
Contributing
See CONTRIBUTING.md. Open areas if you want to dig in:
- Stop / Stop-Limit orders: order type enum and matching engine
- VWAP / TWAP algos:
ExecutionEngine, child order slicing - Trading calendar: holiday/early-close filtering before bars hit the engine
- Multi-strategy portfolio: shared capital across strategies with a meta-allocator
- Parallel sweeps on Linux:
n_jobsexists but Windows spawn overhead kills it; a Linux worker pool would make it actually useful
The engine doesn't handle corporate actions, survivorship bias, or timezone normalization. Feed it clean adjusted data and none of those are problems.
License
MIT: see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quantcore-0.2.1-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 366.8 kB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d775f6e5e8b7b5f08c3ec0c814247260c848fb6f571adaf003135171b253e85
|
|
| MD5 |
68b5dc7cb323fb67f3536cb49f18db90
|
|
| BLAKE2b-256 |
0c917845a478359635f9144c0d17841a69086f23db4deb9298a69e7162a46811
|
File details
Details for the file quantcore-0.2.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.0 MB
- Tags: CPython 3.12, manylinux: glibc 2.26+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5785fdae398bbc5f6a0317bae3af7815ce1771289859a168ea1f766460584bfa
|
|
| MD5 |
c335e16f54d42911f04b104ee028ca90
|
|
| BLAKE2b-256 |
6bfc0430a1c1007198d1fa79214f41fa7387adb13da4ffddad677201f6924a41
|
File details
Details for the file quantcore-0.2.1-cp312-cp312-macosx_15_0_arm64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp312-cp312-macosx_15_0_arm64.whl
- Upload date:
- Size: 320.1 kB
- Tags: CPython 3.12, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6316aae36aa6fc9a6526bad4cb6e08f667218343e498906641e49578799e4f62
|
|
| MD5 |
ec838483fe467557fc0839afce7f3dcb
|
|
| BLAKE2b-256 |
085a9d7836f93e6ebadd37e4993b6afbe3bdefed93b8af116573cb3553739a41
|
File details
Details for the file quantcore-0.2.1-cp311-cp311-win_amd64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp311-cp311-win_amd64.whl
- Upload date:
- Size: 364.2 kB
- Tags: CPython 3.11, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d439e5f365932a0eb2c2b44f258cf6260a8a8882f21a185fb220e43329411cbe
|
|
| MD5 |
c99c40a89e53c4249b8b436849c118db
|
|
| BLAKE2b-256 |
6d710d2eafb6252c4c620d311b53775b58172bdd4f7bb3e7304a0feb40d15644
|
File details
Details for the file quantcore-0.2.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.0 MB
- Tags: CPython 3.11, manylinux: glibc 2.26+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95fb131e0ef8f39f223a20561ccb6cd90a030ade39121b8b466d3e24339d600a
|
|
| MD5 |
9e7f2ab96b0b84fc13b0ee0bf26a2886
|
|
| BLAKE2b-256 |
0c7ceecfe39eb4fd5aa541eddcdb96b94c678260c9f43ed5469a99ddd19395c6
|
File details
Details for the file quantcore-0.2.1-cp311-cp311-macosx_15_0_arm64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp311-cp311-macosx_15_0_arm64.whl
- Upload date:
- Size: 318.5 kB
- Tags: CPython 3.11, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b24eb9c1b121531d1c5910dd10da122aedd17f60ac4704d0b222f5245657a5aa
|
|
| MD5 |
e3583f5c3178c99f152216c5d5edde3f
|
|
| BLAKE2b-256 |
ae768261b8d55c0eae9f796149787e1c885cf74856762d07015de51293f8ef94
|
File details
Details for the file quantcore-0.2.1-cp310-cp310-win_amd64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp310-cp310-win_amd64.whl
- Upload date:
- Size: 363.4 kB
- Tags: CPython 3.10, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aace3cb60f9c221dba314b6f0ebe368b68865960d705b340b93692aabfaf2771
|
|
| MD5 |
625fcfb43dfc9113891940897c7da800
|
|
| BLAKE2b-256 |
b2c738d6740a7ceb3700203d02e2fbb4a4af5970e7a8a2fbb93001812e1ae75d
|
File details
Details for the file quantcore-0.2.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.0 MB
- Tags: CPython 3.10, manylinux: glibc 2.26+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1d55b52902bd89b35cde9151e0f87991c90b4d674321b8b57ee364579c3d832
|
|
| MD5 |
24585eadab55fd63322dc75f34c99681
|
|
| BLAKE2b-256 |
e85af7912677b97ee22dc376895d85f58b4be3a63a376864ee45c794916940f0
|
File details
Details for the file quantcore-0.2.1-cp310-cp310-macosx_15_0_arm64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp310-cp310-macosx_15_0_arm64.whl
- Upload date:
- Size: 317.4 kB
- Tags: CPython 3.10, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a1c69e1edbfcc3471c668fbe14705b19e3d05901d95981ca5487d496e6edd4b
|
|
| MD5 |
13fbb5f0cf81a4dcebd6d7b096984917
|
|
| BLAKE2b-256 |
e87ab16d3bb12bf00b32d9c67d4e3231bde6856400f2d14ffae81bc0c1e15a9d
|
File details
Details for the file quantcore-0.2.1-cp39-cp39-win_amd64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp39-cp39-win_amd64.whl
- Upload date:
- Size: 380.6 kB
- Tags: CPython 3.9, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36d3d6e923bb9a6cc5ee91773d3cf89f7ec3f8f8d69d78c10e909574840898f3
|
|
| MD5 |
10ef00c4de987db92a20945fc0c8a424
|
|
| BLAKE2b-256 |
8a3ac83645a573123db7b13ac8168587e86f7ba1970f09aef5c310e9d2618aa6
|
File details
Details for the file quantcore-0.2.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.0 MB
- Tags: CPython 3.9, manylinux: glibc 2.26+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3adb53e88c4437c66c293179bbfeaf9904953ea98f1f8b193ae74577dd29d3e8
|
|
| MD5 |
80776ca15606ecbfa5ef289d3499c65e
|
|
| BLAKE2b-256 |
d37aac82ba4b6dbe87b05bcc9bc4f62c279dd4a4854d0d47827eaf301b67dc15
|
File details
Details for the file quantcore-0.2.1-cp39-cp39-macosx_15_0_arm64.whl.
File metadata
- Download URL: quantcore-0.2.1-cp39-cp39-macosx_15_0_arm64.whl
- Upload date:
- Size: 317.6 kB
- Tags: CPython 3.9, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
131415e19b3aa9d254a301a8cb1036a0412e82fa1837ff7d9ee1efb5ac677a40
|
|
| MD5 |
603e85b886101673bbc136ea9688b5fd
|
|
| BLAKE2b-256 |
1577ffa2ae422e736ca400d4ffa43d775cd648f4f29c97f446a5c99fa93206cb
|