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

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.

img.png


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_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. 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.2.0-cp312-cp312-win_amd64.whl (363.8 kB view details)

Uploaded CPython 3.12Windows x86-64

quantcore-0.2.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl (1.0 MB view details)

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

quantcore-0.2.0-cp312-cp312-macosx_15_0_arm64.whl (317.0 kB view details)

Uploaded CPython 3.12macOS 15.0+ ARM64

quantcore-0.2.0-cp311-cp311-win_amd64.whl (361.1 kB view details)

Uploaded CPython 3.11Windows x86-64

quantcore-0.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl (1.0 MB view details)

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

quantcore-0.2.0-cp311-cp311-macosx_15_0_arm64.whl (315.5 kB view details)

Uploaded CPython 3.11macOS 15.0+ ARM64

quantcore-0.2.0-cp310-cp310-win_amd64.whl (360.4 kB view details)

Uploaded CPython 3.10Windows x86-64

quantcore-0.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl (1.0 MB view details)

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

quantcore-0.2.0-cp310-cp310-macosx_15_0_arm64.whl (314.4 kB view details)

Uploaded CPython 3.10macOS 15.0+ ARM64

quantcore-0.2.0-cp39-cp39-win_amd64.whl (377.6 kB view details)

Uploaded CPython 3.9Windows x86-64

quantcore-0.2.0-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl (1.0 MB view details)

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

quantcore-0.2.0-cp39-cp39-macosx_15_0_arm64.whl (314.6 kB view details)

Uploaded CPython 3.9macOS 15.0+ ARM64

File details

Details for the file quantcore-0.2.0-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: quantcore-0.2.0-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 363.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

Hashes for quantcore-0.2.0-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 b9899ee24237ec1788e8377ce826574469ce6fbabb898bf9282da71e90ef8d09
MD5 d5e18f19d4612d87a1e330604810f38e
BLAKE2b-256 d7a341877b64e956dddb592865876cee830c0d24e7653a4315f182dba8a26b29

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 440b8629b08381f0c7853df732d2e1bafc8de53930a21acd8fbdbbc965bd101f
MD5 e43d16a738a9f9be910cb3536c85a23e
BLAKE2b-256 ed725584d35110c9ea7b8a48b93eeb17a1618dc063394a4a27601ef21c68aa9f

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp312-cp312-macosx_15_0_arm64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp312-cp312-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 df5ec43ecc2ebf89c2a507be20131f9f19f6236b15f1efff781ff28c611be9d7
MD5 02338224a19cde6dce72572c938ded73
BLAKE2b-256 9f40b51040076e4899c6792685db2bf049b55161ad3ef36c237f7d1a28ebfb85

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: quantcore-0.2.0-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 361.1 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

Hashes for quantcore-0.2.0-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 041c80e3c2271cf6a17055418ac2c8e1265e2f9b357600c2d1a51cbd421d05af
MD5 8a06e139d645a353373d0873e2da74a3
BLAKE2b-256 2214bcf7bd183ae2e56dc26fbd13f20e0044f35178c03179b00aa4cefb46f4fb

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 45eb3cffa894a63c94b054e38336da87672ae4e7dd62c72daadf40c7773c0f12
MD5 90e95fae81c01e6fb1e38e453597966d
BLAKE2b-256 a401f25d6caabf0a8301f3e2c995b1755c8f3dc24bb648f0579cdbf9d9cbd29b

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp311-cp311-macosx_15_0_arm64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp311-cp311-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 43e1f61c9e48903d593ec4f729296e15039b3de0cc872a503b434774609a9284
MD5 34b0c7b104d763e43f32400a30c971d5
BLAKE2b-256 a3dc97e68e8b4e30d16b86301fcbad315fbbcbf51bc65e4830bdce205a4cd6aa

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp310-cp310-win_amd64.whl.

File metadata

  • Download URL: quantcore-0.2.0-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 360.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

Hashes for quantcore-0.2.0-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 40fae7246e0e6ceeaf06d973f767deb548ada8f486f69585f21789bc5d405697
MD5 a29e2672ce92c0cdcadc7972b7caea4e
BLAKE2b-256 7d80a102cd9fd00f5a0c8387e229c006e06aac77e5708ee34c4b2765eaa397e2

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 bfe1aaefde9fa61b095170e70b9d388ab520f88f154538eb5599544ae7688a95
MD5 e4a470399d708ceb302f3d2aa6803496
BLAKE2b-256 dff7d0a5d3311beac8da4b8abba287264c5ee7ee932a820368443dfc364841f4

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp310-cp310-macosx_15_0_arm64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp310-cp310-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 2466248daa1c63f4cca6c91cdd3535feb21c1b2d9a05df2da4bd71cf58ea1b5b
MD5 2aff60bc92be1fc338111ab3db205fe7
BLAKE2b-256 8887f26dfa2338d866d5c9e848a6ce32a991960503773d076f1e1e64ebb2e844

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp39-cp39-win_amd64.whl.

File metadata

  • Download URL: quantcore-0.2.0-cp39-cp39-win_amd64.whl
  • Upload date:
  • Size: 377.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

Hashes for quantcore-0.2.0-cp39-cp39-win_amd64.whl
Algorithm Hash digest
SHA256 417a39718c628251485df4b33737cf79d345ea1f796defebc8cad2d9aa621404
MD5 fee81432b3fa5e33528807fe97161207
BLAKE2b-256 36bd2b2fb171dd3f6af7eb1ad392d70c4c903eedf0295bb36adb5177422f7ff1

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 143c2c8215b4c18494d472dd0815c62292688d5d11314ef6f926401dd3e4f432
MD5 04af6c9087995ffa56fe85c2d94caa1b
BLAKE2b-256 9d41b12a51c47dce2dd9fed42f145089d83e35ae4151d4548db1317e0e71c71c

See more details on using hashes here.

File details

Details for the file quantcore-0.2.0-cp39-cp39-macosx_15_0_arm64.whl.

File metadata

File hashes

Hashes for quantcore-0.2.0-cp39-cp39-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 e3356df60c0988f66473f76ef963ec2ee7b6d9e8785aaef7cb4e6d614134efba
MD5 755263c14997ebee918e7f47f30bf352
BLAKE2b-256 e91d48d620981ffd305abb2a2c4ff0a6afb214c6a64e52ff13ee00ff2ce6043a

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