A quantitative-finance library with multithreaded C++ compute kernels and a clean, typed Python API.
QuantCore implements the building blocks of Monte Carlo quantitative finance — Brownian motion, geometric Brownian motion, option pricing, Markov chains, and statistics — with the numerically heavy parts written in C++ and exposed to Python through pybind11. The Python layer stays thin and well-typed; the C++ layer does the work and parallelises across threads when it pays off.
import quantcore as qc
# Price a European call two ways and watch them agree
bs = qc.black_scholes(s0=100, K=100, r=0.05, sigma=0.2, T=1.0, option_type="call")
mc = qc.mc_european(s0=100, K=100, r=0.05, sigma=0.2, T=1.0, n_paths=1_000_000)
print(f"Black-Scholes : {bs:.4f}")
print(f"Monte Carlo : {mc.price:.4f} ± {mc.std_error:.4f}")
# Black-Scholes : 10.4506
# Monte Carlo : 10.4565 ± 0.0104- Brownian motion — paths and raw increments, seeded and reproducible, with a multithreaded variant that scales to ~5× on 8 cores.
- Geometric Brownian motion — exact lognormal discretisation (no Euler bias), plus memory-light terminal-value sampling for pricing.
- Option pricing — closed-form Black-Scholes and a risk-neutral Monte Carlo pricer with antithetic variates and a reported standard error.
- Markov chains — discrete-state simulation from a validated transition matrix, with an analytic stationary-distribution helper.
- Statistics — mean/variance/std, covariance & correlation matrices, OLS regression with R², and confidence intervals — all in C++, all checked against NumPy.
- Engineering — deterministic RNG streams, strict input validation, a 100+-test pytest suite, benchmark scripts, runnable examples, and CI on Linux and macOS across Python 3.10–3.13.
The project is a study in bridging Python and C++ cleanly:
- The C++ compute functions are independent of pybind11 — they take and
return plain
std::vectors. A single boundary file (binding.cpp) handles all NumPy ↔ C++ conversion, so the math stays testable and portable. - Randomness is deterministic and thread-safe: each thread derives its own
RNG stream from
(seed, thread_id)and writes into a disjoint slice of the output, so results are reproducible for a given(seed, n_threads)and the single-threaded result is reproduced exactly whenn_threads = 1. - Every numerical routine is validated against a trusted reference (NumPy or a closed-form result), not just against itself.
Requires a C++17 compiler, CMake, and Python ≥ 3.10. The build (CMake +
pybind11) is driven by scikit-build-core, so a normal pip install just works:
git clone https://github.com/RonithManikonda/quantcore.git
cd quantcore
python -m venv .venv && source .venv/bin/activate
python -m pip install -e ".[dev]" # builds the C++ extension + dev tools
pytest -q # run the test suiteimport quantcore as qc
# 1,000 paths, 252 daily steps, 20% vol — shape (1000, 253), every path starts at 0
paths = qc.brownian_paths(n_steps=252, n_paths=1_000, dt=1/252, sigma=0.2, seed=42)
# Same contract, parallelised across path batches (0 = auto-detect cores)
fast = qc.brownian_paths_parallel(n_steps=252, n_paths=1_000_000, dt=1/252,
sigma=0.2, seed=42, n_threads=8)
# Raw increments dW ~ N(0, sigma^2 dt); cumsum + a leading 0 reconstructs a path
inc = qc.brownian_increments(n_steps=252, n_paths=1_000, dt=1/252, seed=42)# Asset-price paths: dS = mu S dt + sigma S dW, starting at s0, always positive
prices = qc.gbm_paths(n_steps=252, n_paths=10_000, dt=1/252,
mu=0.08, sigma=0.2, s0=100.0, seed=0)
# Terminal prices only, drawn straight from the lognormal law (memory-light)
terminal = qc.gbm_terminal(n_paths=1_000_000, T=1.0, mu=0.08, sigma=0.2, s0=100.0)call = qc.black_scholes(s0=100, K=105, r=0.05, sigma=0.2, T=1.0, option_type="call")
put = qc.black_scholes(s0=100, K=105, r=0.05, sigma=0.2, T=1.0, option_type="put")
res = qc.mc_european(s0=100, K=105, r=0.05, sigma=0.2, T=1.0,
n_paths=500_000, option_type="call", antithetic=True)
print(res.price, res.std_error) # MCResult(price=..., std_error=...)import numpy as np
P = np.array([[0.7, 0.2, 0.1],
[0.3, 0.4, 0.3],
[0.2, 0.45, 0.35]])
chains = qc.markov_chain(P, n_steps=1_000, n_chains=500, start_state=0, seed=7)
pi = qc.stationary_distribution(P) # long-run state probabilitiesimport numpy as np
rng = np.random.default_rng(0)
X = rng.normal(size=(1000, 3))
y = X @ [1.5, -2.0, 0.5] + 0.1 * rng.normal(size=1000)
cov = qc.covariance(X) # (3, 3) covariance matrix
corr = qc.correlation(X) # (3, 3) correlation matrix
fit = qc.linear_regression(X, y) # RegressionResult(intercept, coefficients, r_squared)
ci = qc.confidence_interval(y, confidence=0.95) # ConfidenceInterval(low, high)Runnable versions of all of the above live in examples/.
Measured on an Apple M4 Pro (14 cores) from Python — full details and tables in BENCHMARKS.md.
| Workload | Result |
|---|---|
| Brownian paths, parallel scaling (compute-heavy) | 5.3× on 8 threads |
| Monte Carlo pricing throughput | ~77–90M paths/second |
| MC error vs Black-Scholes (10M paths) | within 0.003 |
| Antithetic variates | ~1.42× lower standard error (≈ 2× the paths, free) |
The benchmarks deliberately include a case where threading doesn't help (tiny workloads, where launch overhead dominates) — knowing when not to parallelise is part of the point.
quantcore/
├── src/
│ ├── cpp/ # C++ compute kernels (kept pybind11-free)
│ │ ├── binding.cpp # the single pybind11 boundary (all NumPy <-> C++)
│ │ ├── brownian.{hpp,cpp} # Brownian motion (serial + parallel)
│ │ ├── gbm.{hpp,cpp} # geometric Brownian motion
│ │ ├── pricing.{hpp,cpp} # Black-Scholes + Monte Carlo
│ │ ├── markov.{hpp,cpp} # Markov chains
│ │ ├── stats.{hpp,cpp} # covariance, correlation, OLS, CI
│ │ ├── rng.hpp # seeded + per-thread RNG streams
│ │ ├── parallel.hpp # thread-count policy
│ │ ├── special.hpp # normal CDF / inverse CDF
│ │ └── utils.hpp # input validation
│ └── python/quantcore/ # thin, typed Python API over _core
│ ├── stochastic.py # Brownian + GBM
│ ├── pricing.py · markov.py · stats.py
│ └── py.typed
├── tests/ # pytest suite (104 tests)
├── benchmarks/ # bench_brownian · bench_parallel · bench_pricing
├── examples/ # runnable demos for every feature
├── CMakeLists.txt · pyproject.toml
├── BENCHMARKS.md · CHANGELOG.md · LICENSE
- Shapes. Simulators return
(n_paths, n_steps + 1)for paths (column 0 is the start value) and(n_paths, n_steps)for increments. Everything isfloat64NumPy, except Markov state trajectories which are integer. - Seeds. Every stochastic function takes a
seed; identical arguments produce identical output. Parallel runs are deterministic per(seed, n_threads). - Validation. Invalid inputs (non-positive
dt/steps/paths, negative volatility, a transition matrix whose rows don't sum to 1, …) raiseValueErrorwith a message naming the offending parameter. - Threads.
n_threads = 0auto-detects the core count and is capped atn_paths.
pytest -q # all 104 tests
pytest tests/test_pricing.py -vTests cover output shapes, seed reproducibility, input validation, statistical
properties (variance growth, lognormal moments, transition frequencies), and
direct cross-checks against NumPy (cov, corrcoef, lstsq) and closed-form
Black-Scholes values.
- Variance-reduction beyond antithetics (control variates, QMC / Sobol)
- American / path-dependent payoffs (Longstaff–Schwartz)
- Additional processes (Ornstein–Uhlenbeck, Heston stochastic volatility)
- Parallelised Markov simulation across chains
MIT © Ronith Manikonda