https://github.com/nktkt/pricer
pricer — a scalable pricing & risk engine in C++ with an LLVM JIT payoff compiler
https://github.com/nktkt/pricer
cpp jit llvm monte-carlo option-pricing quant quantitative-finance
Last synced: 30 days ago
JSON representation
pricer — a scalable pricing & risk engine in C++ with an LLVM JIT payoff compiler
- Host: GitHub
- URL: https://github.com/nktkt/pricer
- Owner: nktkt
- License: mit
- Created: 2026-05-22T13:02:52.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-22T15:08:10.000Z (about 1 month ago)
- Last Synced: 2026-05-22T18:50:25.244Z (about 1 month ago)
- Topics: cpp, jit, llvm, monte-carlo, option-pricing, quant, quantitative-finance
- Language: C++
- Size: 358 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Roadmap: ROADMAP.md
Awesome Lists containing this project
README
# pricer
A header-only C++17 quant library: Black–Scholes, Monte Carlo, American /
Bermudan (early-exercise), path-dependent exotics (Asian / barrier / lookback / digital)
and multi-asset baskets / spreads / rainbows pricing, a Bachelier (normal) model
for negative rates, model calibration (SVI, SABR, Heston, Dupire) and exact Greeks by
automatic differentiation, counterparty risk (xVA), and a **runtime payoff
compiler built on LLVM JIT** — scaled across SIMD lanes, CPU cores and processes.
Every header is small and heavily commented, with a runnable example and a test
for each topic. Together they walk from the basics of pricing to the kind of
just-in-time, vectorized, distributed engines used in quantitative finance.
This is growing toward a single goal: a **scalable pricing & risk engine** where
you describe an instrument as a formula and get fast, correct results — on a
laptop or across a cluster. See [`ROADMAP.md`](ROADMAP.md) for the long-range plan.
📖 **API reference:** https://nktkt.github.io/pricer/ (generated by Doxygen, published from CI).
🏗️ **Design:** [`ARCHITECTURE.md`](ARCHITECTURE.md) · 🤝 **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md)
## Project layout
```
include/pricer/ header-only core library
normal.hpp standard-normal pdf / cdf
black_scholes.hpp closed-form pricing + Greeks (continuous dividend yield q)
monte_carlo.hpp generic terminal-value MC engine
american.hpp American options: CRR binomial tree + Longstaff–Schwartz LSM
bermudan.hpp Bermudan options: LSM over a finite (any) exercise schedule
basket.hpp multi-asset options: basket (correlated GBM) + spread / exchange (Margrabe, Kirk)
digital.hpp digital (binary) options: cash-or-nothing / asset-or-nothing (closed form + MC)
rainbow.hpp rainbow (best-of / worst-of) two-asset options + bivariate normal CDF (Stulz)
bachelier.hpp Bachelier (normal) model: pricing + Greeks + implied vol (handles negative rates)
exotics.hpp path-dependent exotics: Asian / barrier / lookback (closed form + MC w/ BGK)
parallel.hpp deterministic multithreaded MC (result independent of thread count)
rng.hpp counter-based RNG (stateless) for parallel/SIMD path generation
simd.hpp portable SIMD layer (GCC/Clang vector ext; vectorized exp/log/sqrt + inv-normal CDF)
simd_mc.hpp SIMD-vectorized counter-based Monte Carlo (W paths per step)
parallel_simd.hpp multicore + SIMD engine (deterministic, thread-count-independent)
variance_reduction.hpp antithetic & control-variate estimators
qmc.hpp quasi-Monte Carlo (low-discrepancy) + inverse-normal CDF
implied_vol.hpp implied-volatility solver (safeguarded Newton)
greeks_mc.hpp Monte Carlo Greeks (bump+CRN and pathwise)
risk.hpp Value-at-Risk / Expected Shortfall from a P&L sample
xva.hpp exposure-simulation scenario engine + CVA / DVA / bilateral CVA
curve.hpp discount (yield) curve: zero rate / df / forward
vol_surface.hpp implied-vol surface from a market price grid (bilinear)
smile.hpp least-squares quadratic volatility-smile calibration
optimize.hpp Levenberg–Marquardt least-squares solver (numerical Jacobian)
svi.hpp SVI smile model + least-squares calibration (uses optimize.hpp)
quadrature.hpp Gauss–Legendre numerical integration
heston.hpp Heston stochastic-vol pricing (char. function) + calibration
sabr.hpp SABR smile model: Hagan implied-vol + Black-76 + calibration + SDE MC
dual.hpp forward-mode automatic differentiation (dual numbers)
greeks_ad.hpp exact Black–Scholes Greeks via AD (matches closed form)
adjoint.hpp reverse-mode AD (AAD): all first-order Greeks in one sweep
portfolio.hpp book-level Greeks: a multi-name book's risk from one AAD sweep
local_vol.hpp Dupire local volatility from a call-price surface
distributed.hpp block-sharded MC with deterministic global aggregation
csv.hpp minimal CSV read/write
market_data.hpp CSV market-data adapters + result persistence
payoff_ast.hpp payoff DSL: tokenizer, parser, typed AST + interpreter (no LLVM)
payoff_jit.hpp walks the AST to emit LLVM IR and JIT-compile (needs LLVM)
examples/ runnable demos built on the library
tests/ CTest suite (dependency-free; DSL test needs LLVM)
python/ Python bindings (pybind11): `pip install .`
server/ REST pricing service (POSIX sockets, no dependencies)
```
| Example | Topic | Highlight |
|---------|-------|-----------|
| `black_scholes_demo` | Black–Scholes vs. Monte Carlo | Two independent methods agree to ~0.05% |
| `convergence` | Accuracy vs. speed | Error shrinks like `1/sqrt(N)` (×100 paths → ~1/10 error) |
| `parallel_mc` | Multithreaded Monte Carlo | ~8× faster across 10 logical cores |
| `benchmark` | MC throughput harness | ~275 Mpaths/s; baseline for tracking optimizations |
| `greeks` | Risk sensitivities (Greeks) | Closed-form vs. finite-difference cross-check |
| `barrier_option` | Path-dependent product | Up-and-out barrier call via stepped MC |
| `american_option` | Early-exercise options | American put: CRR binomial tree vs. Longstaff–Schwartz LSM; early-exercise premium |
| `bermudan_option` | Finite exercise schedule | Bermudan put climbs from the European to the American value as exercise dates fill in |
| `basket_options` | Multi-asset (correlated GBM) | Basket and spread/exchange options: geometric closed form, Margrabe & Kirk vs. Monte Carlo |
| `digital_options` | Binary options | Cash-/asset-or-nothing closed form vs. MC; the exact vanilla decomposition |
| `rainbow_options` | Two-asset best-of/worst-of | Stulz closed form for max/min options vs. MC; the call-max + call-min = two-vanillas parity |
| `bachelier_demo` | Normal (Bachelier) model | Normal-vol pricing/Greeks/implied-vol vs. MC; prices a negative-rate floorlet |
| `exotic_options` | Path-dependent exotics | Asian / barrier / lookback: each closed form vs. Monte Carlo (BGK-corrected) agree |
| `payoff_interpret` | Payoff DSL without LLVM | Parse a formula → typed AST → tree-walking interpreter |
| `jit_payoff` | **LLVM JIT** payoff compiler | Parses a formula string → LLVM IR → native code at runtime |
| `path_dependent` | Exotics from formulas | Asian / barrier / lookback / digital, each a one-line formula |
| `simd_payoff` | Vectorized codegen | Same formula compiled to `` SIMD IR; scalar vs. batch |
| `variance_reduction` | Fewer paths, same accuracy | Antithetic / control-variate / QMC error vs. plain MC |
| `mc_greeks` | Monte Carlo Greeks | Bump+CRN and pathwise delta/vega vs. closed form |
| `risk_demo` | Portfolio risk | 1-day VaR / ES of an option book by scenario simulation |
| `pricer_cli` | Command-line tool | `price` / `iv` / `mc` sub-commands |
| `vol_surface_demo` | Curve & vol surface | Discount curve, implied-vol surface, smile calibration |
| `svi_calibration` | SVI model calibration | Fit an SVI smile to market quotes via Levenberg–Marquardt |
| `heston_calibration` | Heston model calibration | Fit Heston to an option grid by least squares |
| `sabr_smile` | SABR smile model | Hagan implied-vol smile, calibration round-trip, and an SDE Monte Carlo cross-check |
| `ad_greeks` | Greeks by auto-diff | Forward-mode AD Greeks vs. closed form (machine precision) |
| `aad_greeks` | Greeks by adjoint AD | All first-order Greeks from one reverse-mode backward sweep |
| `local_vol_demo` | Dupire local vol | Local volatility recovered from a call-price surface |
| `distributed_mc` | Distributed Monte Carlo | Sharded across worker processes; identical price for any worker count |
| `market_data_demo` | Market data & persistence | Load quotes (CSV) → imply vol & Greeks → save results (CSV) |
| `simd_paths` | SIMD path generation | Scalar vs. vectorized RNG: counter-based ~1.65×, SIMD ~2.2× over std::mt19937 |
| `scale_benchmark` | Single-node speedup ladder | mt19937 → counter-based → SIMD → multicore; **~12.8× over the Phase-1 baseline** |
| `xva_demo` | Counterparty risk (xVA) | Exposure profile → CVA / DVA / BCVA of a European call |
| `portfolio_aad` | Book risk in one sweep | A multi-name book's value + every delta/vega from one reverse-mode AAD pass |
## Requirements
- CMake 3.16+ and a C++17 compiler (`clang++` or `g++`)
- **LLVM** (optional, only for the `jit_payoff` / `path_dependent` examples and the
DSL test). On macOS: `brew install llvm`
## Build, test & run
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure # run the test suite
./build/examples/black_scholes_demo # run a demo
```
The JIT examples are built when `PRICER_ENABLE_JIT` is on (default) **and** CMake
finds LLVM (point it there with `-DLLVM_DIR=$(llvm-config --cmakedir)` if needed);
otherwise they are skipped and the rest still builds. CI builds and tests on Linux
and macOS with `-DPRICER_ENABLE_JIT=OFF` (the hosted runners expose an LLVM whose
JIT cannot initialize there), so the JIT path is exercised locally instead.
## The LLVM JIT highlight
`jit_payoff` takes a payoff **formula as a string**, turns it into LLVM IR,
JIT-compiles it to native code at runtime, and calls it from a Monte Carlo loop.
Change the formula and you price a different instrument — without recompiling C++.
```sh
./build/examples/jit_payoff # call: max(ST - K, 0) (matches Black–Scholes)
./build/examples/jit_payoff "max(K - ST, 0)" # put
./build/examples/jit_payoff "max(ST-K,0)+max(K-ST,0)" # straddle
./build/examples/jit_payoff "(ST > K) * 10" # cash-or-nothing digital
```
For `max(ST - K, 0)` (variables read from the `double* v` argument) it generates:
```llvm
define double @payoff_0(ptr %v) {
entry:
%0 = getelementptr inbounds double, ptr %v, i64 0
%1 = load double, ptr %0, align 8 ; ST
%2 = getelementptr inbounds double, ptr %v, i64 1
%3 = load double, ptr %2, align 8 ; K
%4 = fsub double %1, %3
%5 = fcmp ogt double %4, 0.000000e+00
%6 = select i1 %5, double %4, double 0.000000e+00
ret double %6
}
```
`path_dependent` goes further: the engine simulates whole paths and exposes
`ST`, `avg`, `Smax`, `Smin` (plus `K`) to the DSL, so exotics become one-liners —
e.g. an arithmetic Asian call is `max(avg - K, 0)` and an up-and-out barrier is
`max(ST - K, 0) * (Smax < 130)`.
Two more `PayoffJit` features round out the engine:
- **Compiled-kernel cache** — `compile()` is keyed by `(width, variables, formula)`,
so repeating a formula returns the existing function pointer with no recompile.
- **Vectorized codegen** — `compile_batch(formula, vars, W)` emits a
`void payoff_v(const double* v, double* out)` kernel that evaluates `W` paths at
once with `` IR (see `simd_payoff`). The same parser produces scalar
or vector code; only the leaves (constant splats, vector loads) change.
Supported grammar: numbers, variables (whichever names you bind), operators
`+ - * /` and unary minus, comparisons `< > <= >= == !=` (yielding `1.0`/`0.0`),
parentheses, and functions `exp log sqrt abs` (1 arg), `max min pow` (2 args),
`if(cond, a, b)` (3 args).
> Note: the DSL parser uses exceptions for error reporting, so the `jit_payoff`
> target is compiled with `-fexceptions` (LLVM itself often ships built without).
## Performance & scale
Reaching a target accuracy faster — by going wider (vectorize, then multicore),
and by needing fewer paths:
- **SIMD path generation** (`pricer/simd.hpp`, `pricer/simd_mc.hpp`) generates `W`
paths at a time on a portable vector layer built on the GCC/Clang vector
extensions (AVX2 on x86, NEON on ARM) — vectorized RNG, inverse-normal CDF and
`exp`. The counter-based RNG is what makes this possible: draw `i` is a pure
function of `(seed, i)`, so a batch of counters fills one SIMD register. ~2.2×
over a stateful `std::mt19937_64` baseline.
- **Deterministic parallel MC** (`pricer/parallel.hpp`) splits work into a fixed
set of blocks with per-block seeds, so the result is bit-for-bit identical no
matter how many threads run it. Throughput scales with cores (~7× on 10 cores).
- **Multicore + SIMD** (`pricer/parallel_simd.hpp`) stacks both axes: SIMD path
generation across all cores, still bit-identical for any thread count. The
`scale_benchmark` example walks the full ladder and reaches **~12.8× over the
single-threaded Phase-1 baseline on a 10-core machine** — clearing the roadmap's
≥10× CPU target.
- **Variance reduction** (`pricer/variance_reduction.hpp`, `pricer/qmc.hpp`):
antithetic and control-variate estimators, plus quasi-Monte Carlo whose error
decays roughly like `1/N` instead of `1/sqrt(N)`. For a vanilla call these reach
the analytic price with 1–3 orders of magnitude less error than plain MC at the
same path count (run `variance_reduction`).
GPU offload (NVPTX/SPIR-V) is on the roadmap but needs appropriate hardware/CI,
so it is not built here yet.
## Risk & calibration
From a price to the risk around it:
- **Greeks, exactly.** Closed-form Black–Scholes Greeks, forward-mode AD
(`dual.hpp`, machine precision) and reverse-mode adjoint AD (`adjoint.hpp`) that
returns all first-order Greeks from one backward sweep. `portfolio.hpp` scales
that to a book: `book_greeks_aad` prices a multi-name book on one tape and
returns its value and every position's delta and vega from a single sweep — at a
cost independent of the number of inputs (`portfolio_aad`).
- **Curves, surfaces & calibration.** A discount curve (`curve.hpp`), an
implied-vol surface (`vol_surface.hpp`), and least-squares calibration of a
quadratic smile, an SVI smile and the Heston model via Levenberg–Marquardt
(`optimize.hpp`, `smile.hpp`, `svi.hpp`, `heston.hpp`), plus Dupire local
volatility (`local_vol.hpp`).
- **Portfolio risk & xVA.** Value-at-Risk / Expected Shortfall (`risk.hpp`), and
counterparty valuation adjustments (`xva.hpp`): a GBM exposure-simulation
scenario engine feeds CVA, DVA and bilateral CVA computed against a hazard-rate
survival curve and a discount curve (`xva_demo`).
## Command line
`pricer_cli` prices from the shell:
```sh
./build/examples/pricer_cli price --type call --S 100 --K 100 --r 0.05 --sigma 0.2 --T 1
# price: 10.450584 (+ delta/gamma/vega/theta/rho)
./build/examples/pricer_cli iv --type call --price 10.4506 --S 100 --K 100 --r 0.05 --T 1
./build/examples/pricer_cli mc --type call --S 100 --K 100 --r 0.05 --sigma 0.2 --T 1 --paths 5000000
```
## REST service
A dependency-free HTTP service (POSIX sockets) exposes pricing over the network,
including an async Monte Carlo job API:
```sh
cmake -S . -B build -DPRICER_BUILD_SERVER=ON && cmake --build build
./build/server/pricer_server 8080 &
curl 'http://127.0.0.1:8080/price?type=call&S=100&K=100&r=0.05&sigma=0.2&T=1'
# {"price":10.45058357,"delta":0.63683065,...}
curl 'http://127.0.0.1:8080/submit?type=call&S=100&K=100&r=0.05&sigma=0.2&T=1&paths=20000000' # {"job_id":1}
curl 'http://127.0.0.1:8080/job?id=1' # {"status":"done","mc_price":...}
curl 'http://127.0.0.1:8080/metrics' # Prometheus counters: requests, latency, jobs, uptime
```
Endpoints: `/health`, `/price`, `/impliedvol`, `/mc`, `/submit`, `/job`, `/metrics`.
**Observability:** `/metrics` exposes request, latency, job and uptime counters in
the Prometheus text format, and every request is written to stderr as a structured
JSON access log line. `python server/smoke_test.py build/server/pricer_server` runs
an end-to-end check (including `/metrics`).
## Docker
The REST service ships as a container. A multi-stage build compiles it and the
runtime image carries only the binary (the server has no third-party
dependencies), running as a non-root user:
```sh
docker build -t pricer-server .
docker run --rm -p 8080:8080 pricer-server
curl 'http://127.0.0.1:8080/price?type=call&S=100&K=100&r=0.05&sigma=0.2&T=1'
curl http://127.0.0.1:8080/metrics
# or, with compose:
docker compose up --build
```
A CI job builds the image and checks the container serves `/health`, `/price`
and `/metrics`.
For Kubernetes, [`k8s/`](k8s/) has Deployment, Service and HorizontalPodAutoscaler
manifests (`kubectl apply -f k8s/`) — non-root pods, `/health` probes, CPU
autoscaling, and Prometheus scrape annotations. See [`k8s/README.md`](k8s/README.md).
## Python
The core is exposed to Python via pybind11 — price a book without touching C++:
```sh
pip install . # builds the C++ extension (scikit-build-core + pybind11)
python python/example_book.py
```
```python
import pricer
from pricer import OptionType
c = pricer.black_scholes_call(100, 100, 0.05, 0.20, 1.0) # 10.4506
g = pricer.black_scholes_greeks(OptionType.Call, 100, 100, 0.05, 0.20, 1.0)
iv = pricer.implied_vol(OptionType.Call, c, 100, 100, 0.05, 1.0) # ~0.20
mc = pricer.mc_price_parallel(OptionType.Call, 100, 100, 0.05, 0.20, 1.0, n_paths=10_000_000)
rm = pricer.var_es(pnl_list, 0.99) # rm.var, rm.es
```
## Disclaimer
Educational code. Not investment advice and not production-grade financial
software.
## License
MIT