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.1-cp312-cp312-win_amd64.whl (366.8 kB view details)

Uploaded CPython 3.12Windows x86-64

quantcore-0.2.1-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.1-cp312-cp312-macosx_15_0_arm64.whl (320.1 kB view details)

Uploaded CPython 3.12macOS 15.0+ ARM64

quantcore-0.2.1-cp311-cp311-win_amd64.whl (364.2 kB view details)

Uploaded CPython 3.11Windows x86-64

quantcore-0.2.1-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.1-cp311-cp311-macosx_15_0_arm64.whl (318.5 kB view details)

Uploaded CPython 3.11macOS 15.0+ ARM64

quantcore-0.2.1-cp310-cp310-win_amd64.whl (363.4 kB view details)

Uploaded CPython 3.10Windows x86-64

quantcore-0.2.1-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.1-cp310-cp310-macosx_15_0_arm64.whl (317.4 kB view details)

Uploaded CPython 3.10macOS 15.0+ ARM64

quantcore-0.2.1-cp39-cp39-win_amd64.whl (380.6 kB view details)

Uploaded CPython 3.9Windows x86-64

quantcore-0.2.1-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.1-cp39-cp39-macosx_15_0_arm64.whl (317.6 kB view details)

Uploaded CPython 3.9macOS 15.0+ ARM64

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

Hashes for quantcore-0.2.1-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 5d775f6e5e8b7b5f08c3ec0c814247260c848fb6f571adaf003135171b253e85
MD5 68b5dc7cb323fb67f3536cb49f18db90
BLAKE2b-256 0c917845a478359635f9144c0d17841a69086f23db4deb9298a69e7162a46811

See more details on using hashes here.

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

File hashes

Hashes for quantcore-0.2.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 5785fdae398bbc5f6a0317bae3af7815ce1771289859a168ea1f766460584bfa
MD5 c335e16f54d42911f04b104ee028ca90
BLAKE2b-256 6bfc0430a1c1007198d1fa79214f41fa7387adb13da4ffddad677201f6924a41

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for quantcore-0.2.1-cp312-cp312-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 6316aae36aa6fc9a6526bad4cb6e08f667218343e498906641e49578799e4f62
MD5 ec838483fe467557fc0839afce7f3dcb
BLAKE2b-256 085a9d7836f93e6ebadd37e4993b6afbe3bdefed93b8af116573cb3553739a41

See more details on using hashes here.

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

Hashes for quantcore-0.2.1-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 d439e5f365932a0eb2c2b44f258cf6260a8a8882f21a185fb220e43329411cbe
MD5 c99c40a89e53c4249b8b436849c118db
BLAKE2b-256 6d710d2eafb6252c4c620d311b53775b58172bdd4f7bb3e7304a0feb40d15644

See more details on using hashes here.

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

File hashes

Hashes for quantcore-0.2.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 95fb131e0ef8f39f223a20561ccb6cd90a030ade39121b8b466d3e24339d600a
MD5 9e7f2ab96b0b84fc13b0ee0bf26a2886
BLAKE2b-256 0c7ceecfe39eb4fd5aa541eddcdb96b94c678260c9f43ed5469a99ddd19395c6

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for quantcore-0.2.1-cp311-cp311-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 b24eb9c1b121531d1c5910dd10da122aedd17f60ac4704d0b222f5245657a5aa
MD5 e3583f5c3178c99f152216c5d5edde3f
BLAKE2b-256 ae768261b8d55c0eae9f796149787e1c885cf74856762d07015de51293f8ef94

See more details on using hashes here.

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

Hashes for quantcore-0.2.1-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 aace3cb60f9c221dba314b6f0ebe368b68865960d705b340b93692aabfaf2771
MD5 625fcfb43dfc9113891940897c7da800
BLAKE2b-256 b2c738d6740a7ceb3700203d02e2fbb4a4af5970e7a8a2fbb93001812e1ae75d

See more details on using hashes here.

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

File hashes

Hashes for quantcore-0.2.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 f1d55b52902bd89b35cde9151e0f87991c90b4d674321b8b57ee364579c3d832
MD5 24585eadab55fd63322dc75f34c99681
BLAKE2b-256 e85af7912677b97ee22dc376895d85f58b4be3a63a376864ee45c794916940f0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for quantcore-0.2.1-cp310-cp310-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 0a1c69e1edbfcc3471c668fbe14705b19e3d05901d95981ca5487d496e6edd4b
MD5 13fbb5f0cf81a4dcebd6d7b096984917
BLAKE2b-256 e87ab16d3bb12bf00b32d9c67d4e3231bde6856400f2d14ffae81bc0c1e15a9d

See more details on using hashes here.

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

Hashes for quantcore-0.2.1-cp39-cp39-win_amd64.whl
Algorithm Hash digest
SHA256 36d3d6e923bb9a6cc5ee91773d3cf89f7ec3f8f8d69d78c10e909574840898f3
MD5 10ef00c4de987db92a20945fc0c8a424
BLAKE2b-256 8a3ac83645a573123db7b13ac8168587e86f7ba1970f09aef5c310e9d2618aa6

See more details on using hashes here.

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

File hashes

Hashes for quantcore-0.2.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 3adb53e88c4437c66c293179bbfeaf9904953ea98f1f8b193ae74577dd29d3e8
MD5 80776ca15606ecbfa5ef289d3499c65e
BLAKE2b-256 d37aac82ba4b6dbe87b05bcc9bc4f62c279dd4a4854d0d47827eaf301b67dc15

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for quantcore-0.2.1-cp39-cp39-macosx_15_0_arm64.whl
Algorithm Hash digest
SHA256 131415e19b3aa9d254a301a8cb1036a0412e82fa1837ff7d9ee1efb5ac677a40
MD5 603e85b886101673bbc136ea9688b5fd
BLAKE2b-256 1577ffa2ae422e736ca400d4ffa43d775cd648f4f29c97f446a5c99fa93206cb

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