Skip to main content

High-performance C++20 backtesting engine with Python interface

Project description

QuantCore

Build Coverage C++ License

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

Quick Start

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('AAPL', 'data/aapl.csv')},
    initial_capital=100_000.0,
)
print(results)

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)

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.

img.png


Strategy Development

Python Strategy

Subclass qc.Strategy and implement on_data. Signals drive order execution. You don't place orders directly, you generate signals and the engine handles the rest.

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.


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

These are raw order book operations with no engine overhead. The matching engine is not the bottleneck at daily-bar scale.

End-to-end backtest

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 -

Throughput is stable across dataset sizes. A 1-year daily backtest completes in under 1 ms at p99.

Run the benchmarks yourself:

cmake --build build --target bench_backtest_engine
./build/bench_backtest_engine

python benchmarks/bench_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
│   ├── 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
│   ├── bindings.cpp          # pybind11 bindings
│   └── build_module.py       # Build helper
├── examples/                 # Jupyter notebooks
├── benchmarks/               # Benchmark suite
├── CMakeLists.txt
└── README.md

vs. Alternatives

QuantCore Backtrader Zipline
Core language C++20 Python Python
Order book simulation ✅ Real LOB
Event-driven
Look-ahead prevention ✅ Priority queue
Python strategy API ✅ pybind11 ✅ native ✅ native
Throughput (bars/s) ~270 K ~50 K ~100 K
Maintenance Active Stale Inactive

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
  • Tick data pipeline: the engine is bar-agnostic internally; the data loader needs extending
  • 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_jobs exists 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. That's the data layer's job. Feed it clean adjusted data and none of those are problems.


License

MIT: see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

quantcore-0.1.0-cp312-cp312-musllinux_1_2_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.12musllinux: musl 1.2+ x86-64

quantcore-0.1.0-cp312-cp312-musllinux_1_2_i686.whl (1.4 MB view details)

Uploaded CPython 3.12musllinux: musl 1.2+ i686

quantcore-0.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (455.1 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

quantcore-0.1.0-cp311-cp311-musllinux_1_2_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.11musllinux: musl 1.2+ x86-64

quantcore-0.1.0-cp311-cp311-musllinux_1_2_i686.whl (1.4 MB view details)

Uploaded CPython 3.11musllinux: musl 1.2+ i686

quantcore-0.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (455.3 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

quantcore-0.1.0-cp310-cp310-musllinux_1_2_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.10musllinux: musl 1.2+ x86-64

quantcore-0.1.0-cp310-cp310-musllinux_1_2_i686.whl (1.4 MB view details)

Uploaded CPython 3.10musllinux: musl 1.2+ i686

quantcore-0.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (454.1 kB view details)

Uploaded CPython 3.10manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

quantcore-0.1.0-cp39-cp39-musllinux_1_2_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.9musllinux: musl 1.2+ x86-64

quantcore-0.1.0-cp39-cp39-musllinux_1_2_i686.whl (1.4 MB view details)

Uploaded CPython 3.9musllinux: musl 1.2+ i686

quantcore-0.1.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (454.4 kB view details)

Uploaded CPython 3.9manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

File details

Details for the file quantcore-0.1.0-cp312-cp312-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 8ef8d7a46d590c5cf1dcc417e4eb933f7059710e05d3ac65d2fba7d6fc9f7d1e
MD5 1e13288d966a775a04d19ad34f356d24
BLAKE2b-256 593d70a5373d81b15b8c003b5370947077a67ad7f5034c00bd646941901bdee5

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp312-cp312-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp312-cp312-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 4483a6f23dda245e6c4588e043fcf6d119ec3c95328ec1fa7df7ac1d25301745
MD5 bea12b3f0c16f40f3a855a7edbc49058
BLAKE2b-256 5dc787e55aab0f990c3119fd58f0c77d21f799ccf1006e135b6516de2f5e8432

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 720eb612b97533b76e4e6d06df1380e27656702ee874e3a9b6bf4278e13e4336
MD5 92757a7535d2735842517c729bf0da3f
BLAKE2b-256 6be9be203acf0cbdcc0a71ec19565e816242a0e551459cf766a7e0ea16bd4dfa

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp311-cp311-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 b3704c15b79e17f972dcf2f03d6189ffbbe1690c413b09845b0d6ebfde8838fa
MD5 d10fff4b960fc7ca7ec673e487f38aa3
BLAKE2b-256 bb2a8792f12114a6db72e2404ed40339a07e552af889810d4a07e9bbfdbf53e8

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp311-cp311-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp311-cp311-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 2565c116f2d24e7a6d86407dc3bbbc615d7a0b4c815eb12ef0c71b8d7cabf8c4
MD5 3778085e0690e910942328ed60701b71
BLAKE2b-256 6ef762ca39186a73b9172cb868f5e4697b492ee3137cffcea6b470b741eb5575

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 5f9a1ac94ccdca916c2740c31620c651025110ee0f1ecb87aafd28270eebf7dc
MD5 c24e20d6b1f42ff6f3add9a0d2ece02c
BLAKE2b-256 d330d83d65b217231a08e49f6ab8ada96996b6a6fef161616e7619b6fe624da5

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp310-cp310-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 2f1e659b1fc12e4600239bf34777b5ac4531bc63a70d4d98424d4fae6191e277
MD5 bb728068ef6803d268193e830b44006c
BLAKE2b-256 2f0d492e1ed2eb3b088be9d08d7ae8bf604ea1e6864bf7f222cb288e42bcf4d4

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp310-cp310-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp310-cp310-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 5f00f5704114a1c441e1bfe2361804b8667c19226f82eda59ebfe277eb93c088
MD5 a48bfce86dcdc9e4299c41178a4c69c7
BLAKE2b-256 1879345e17c599273f4f0ec601a24cd76a501609ffd32cbad89be1e6a95b6ae3

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 13b12d31043d6d3989fff3af88effb63d4bbe4c2d9a4da742049e91a7e5e66a8
MD5 a3593aa295434567a53cb212672ce3dc
BLAKE2b-256 366f9a36d56cb0a6caa52b219366cbfaed8d1f33945d6a37ba66a6bda9d2624b

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp39-cp39-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp39-cp39-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 5799b909d5a64f66b18145fce9db530e4f31536555b701f250b01a2b8d0bb7b9
MD5 b0a4dfcb11c6d98d87852abb642a11ab
BLAKE2b-256 5061dff1f2fcf957d01c2be05fcc7367ad1e91d1d79696808254eed87d9896b4

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp39-cp39-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp39-cp39-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 671dacfe7c3cfa26fd9bf58ca797ae2f125cee4aa519833c5707e5241a04417d
MD5 30488b8e5889bdb9ce975a8f90204bab
BLAKE2b-256 3c153e6d4966cffd5ceb72a77831ca0010557732b91b66f4b278b9fbdcd1a169

See more details on using hashes here.

File details

Details for the file quantcore-0.1.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.1.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 545964506b51186e85044edc7fc457c197eddf9b73bbe7ffabf70835b302d7f2
MD5 f42d1a913976a7fc73056046364fe38e
BLAKE2b-256 c2fdca30f73675f1863b6c121a0dbf30034b430e7931fed7cd784984dbc62a02

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page