High-performance PDE option pricing engine with AAD Greeks
Project description
NablaQuant
High-Performance PDE Option Pricing Engine
Caution: Educational/Experimental purposes only, no financial advice intended (Shouldn't be treated as a real-time/money production engine)
An institutional-grade, low-latency pricing engine for solving financial PDEs. Built with a bare-metal C++ core, exposed as a Python SDK, with a real-time WebGL visualization dashboard.
Installation
From PyPI (pre-built wheel — no compiler needed):
pip install nabla-quant
This installs the full stack: Python SDK + FastAPI server + live market data (yfinance).
From source (requires CMake ≥ 3.20, C++17 compiler with AVX2):
git clone https://github.com/JaiAnshSB26/NablaQuant.git
cd NablaQuant
cmake -B build -DCMAKE_BUILD_TYPE=Release -DNABLA_BUILD_PYTHON=ON
cmake --build build --parallel
pip install -r python/requirements.txt
export PYTHONPATH=python # Windows: $env:PYTHONPATH = "python"
Quick Start
CLI (nabla)
After pip install nabla-quant the nabla command is available on your PATH.
# ── Price a single option ────────────────────────────────────────────────────
# European call (default)
nabla price --spot 100 --strike 100 --expiry 1.0 --vol 0.20
# European CALL S=100.0 K=100.0 T=1.0y
# price = 10.450544
# Put, with dividend yield
nabla price --spot 100 --strike 100 --expiry 1.0 --rate 0.05 --vol 0.20 \
--type put --div 0.02
# American put (early exercise)
nabla price --spot 80 --strike 100 --expiry 1.0 --vol 0.25 --type put --american
# American PUT S=80.0 K=100.0 T=1.0y
# price = 20.363300
# Barrier: down-and-out call (B=80)
nabla price --spot 100 --strike 100 --expiry 1.0 --vol 0.20 \
--barrier-level 80 --barrier-dir down --barrier-knock out
# Barrier down-out CALL S=100.0 K=100.0 B=80.0 T=1.0y
# price = 10.363585
# ── All Greeks via a single AAD backward sweep ───────────────────────────────
nabla greeks --spot 100 --strike 100 --expiry 1.0 --vol 0.20
# Greeks (AAD) CALL S=100.0 K=100.0 T=1.0y vol=0.2
# price = 10.450544
# delta = 0.636829
# gamma = 0.018762
# theta = -6.414016
# vega = 37.523929
# rho = 53.232470
# JSON output — pipe into jq, scripts, notebooks
nabla greeks --spot 679 --strike 679 --expiry 0.011 \
--rate 0.036 --vol 0.139 --div 0.008 --json
# {"price": 4.05, "delta": 0.511, "gamma": 0.040, ...}
# ── Launch the full-stack live dashboard (Next.js + FastAPI) ─────────────────
# Requires: repo cloned + Node.js installed
nabla dashboard # Next.js :3000 + FastAPI :8000, opens browser
nabla dashboard --fe-port 3001 --be-port 9000 # custom ports
nabla dashboard --no-open # skip browser auto-open
# ── Start API + bundled static dashboard (no Node.js needed, from PyPI) ──────
nabla serve # FastAPI :8000, dashboard at http://localhost:8000
nabla serve --port 9000 # custom port
nabla serve --no-open # skip auto-open (CI / headless)
# ── Run the 16-contract pipeline stress test ─────────────────────────────────
nabla stress
# ── Print installed version ──────────────────────────────────────────────────
nabla version
# nabla-quant 0.1.1
All flags for price and greeks:
| Flag | Short | Default | Description |
|---|---|---|---|
--spot |
-S |
required | Spot price |
--strike |
-K |
required | Strike price |
--expiry |
-T |
required | Time to expiry (years) |
--vol |
-v |
required | Flat / implied volatility |
--rate |
-r |
0.05 |
Risk-free rate |
--div |
-q |
0.00 |
Continuous dividend yield |
--type |
call |
call or put |
|
--nodes |
801 |
PDE grid nodes | |
--steps |
800 |
Time steps | |
--json |
false | Emit JSON instead of human-readable output | |
--american |
false | American-style early exercise (price only) |
|
--barrier-level |
— | Barrier level B (price only) |
|
--barrier-dir |
— | up or down (price only) |
|
--barrier-knock |
— | in or out (price only) |
Phase 2 — Core pricing engine
import nabla_quant as nq
# European call via Crank-Nicolson PDE (801-node SinhMesh, 800 time steps)
result = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20)
print(result.price) # 10.4505 (<0.01% error vs Black-Scholes)
# European put
put = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
option_type="put")
# With continuous dividend yield
div = nq.european(spot=100, strike=100, expiry=2.0, rate=0.05, vol=0.20,
dividend=0.04)
# Higher-accuracy grid
hi = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
num_nodes=1601, num_time_steps=1600)
Phase 2 — American options & local volatility
# American put with early-exercise projection
am = nq.american(spot=80, strike=100, expiry=1.0, rate=0.05, vol=0.25,
option_type="put")
eu = nq.european(spot=80, strike=100, expiry=1.0, rate=0.05, vol=0.25,
option_type="put")
print(f"AM={am.price:.4f} EU={eu.price:.4f} premium={am.price-eu.price:.4f}")
# AM=20.3633 EU=18.2645 premium=2.0988
# Local volatility: CEV (leverage effect, beta<1 → downward skew)
cev = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
vol_model="cev", vol_params={"beta": 0.5})
# Local vol: parametric skew σ(S,t) = σ_atm + skew·ln(S/K) + term·√t
skew = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
vol_model="skew", vol_params={"skew": -0.10, "term": 0.02})
Phase 3 — Greeks via Adjoint Algorithmic Differentiation
# All 5 Greeks in a single backward sweep (same cost as one PDE solve)
g = nq.greeks(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20)
print(f"price={g.price:.4f} delta={g.delta:.4f} gamma={g.gamma:.5f}")
print(f"theta={g.theta:.4f} vega={g.vega:.3f} rho={g.rho:.3f}")
# price=10.4505 delta=0.6368 gamma=0.01876
# theta=-6.4140 vega=37.524 rho=53.232
# Independent bump-and-revalue reference (for validation)
bg = nq.bump_greeks(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20)
print(f"AAD vega={g.vega:.4f} bump vega={bg.vega:.4f}") # agree to <0.01
Phase 4 — Barrier options (8 types)
vanilla = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20)
# Down-and-out call (B=80)
doc = nq.barrier(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
level=80, direction="down", knock="out")
# Down-and-in call — in-out parity: DOC + DIC = vanilla to 6+ decimal places
dic = nq.barrier(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
level=80, direction="down", knock="in")
print(f"DOC={doc.price:.6f} DIC={dic.price:.6f} "
f"sum={doc.price+dic.price:.6f} vanilla={vanilla.price:.6f}")
# Up-and-out call (B=130), optional cash rebate on knock
uoc = nq.barrier(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
level=130, direction="up", knock="out", rebate=2.0)
Phase 5 — FastAPI server
# Start the server
cd NablaQuant/python
uvicorn nabla_quant.api.app:app --host 0.0.0.0 --port 8000
# Price a European call
curl -s -X POST http://localhost:8000/api/v1/price/european \
-H "Content-Type: application/json" \
-d '{"spot":100,"strike":100,"expiry":1.0,"rate":0.05,"volatility":0.20}' \
| python -m json.tool
# Full Greeks (AAD)
curl -s -X POST http://localhost:8000/api/v1/greeks \
-H "Content-Type: application/json" \
-d '{"spot":100,"strike":100,"expiry":1.0,"rate":0.05,"volatility":0.20}'
# {"price":10.4505,"delta":0.6368,"gamma":0.01876,"theta":-6.414,"vega":37.524,"rho":53.232}
# Barrier option
curl -s -X POST http://localhost:8000/api/v1/price/barrier \
-H "Content-Type: application/json" \
-d '{"spot":100,"strike":100,"expiry":1.0,"rate":0.05,"volatility":0.20,
"barrier_level":80,"barrier_direction":"down","barrier_knock":"out"}'
# Live SPY quote + dividends (yfinance, 15-min delayed)
curl "http://localhost:8000/api/v1/market/quote?symbol=SPY"
# Live option chain with implied vols
curl "http://localhost:8000/api/v1/market/chain?symbol=SPY&expiration=2026-04-25"
# Market-calibrated IV surface (up to N expirations)
curl "http://localhost:8000/api/v1/market/vol_surface?symbol=SPY&max_expiries=4"
# Batch pricing — up to 100 contracts in one request
curl -s -X POST http://localhost:8000/api/v1/price/batch \
-H "Content-Type: application/json" \
-d '{"requests":[
{"spot":100,"strike":100,"expiry":1.0,"rate":0.05,"volatility":0.20},
{"spot":100,"strike":110,"expiry":0.5,"rate":0.05,"volatility":0.20,"option_type":"put"}
]}'
All endpoints with their methods:
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/health |
Engine status + version |
| POST | /api/v1/greeks |
All Greeks via AAD |
| POST | /api/v1/price/european |
European call/put |
| POST | /api/v1/price/american |
American call/put (early exercise) |
| POST | /api/v1/price/barrier |
Barrier option (all 8 types) |
| POST | /api/v1/price/local_vol |
Local volatility pricing |
| POST | /api/v1/price/batch |
Batch pricing (≤ 100 contracts) |
| GET | /api/v1/market/quote |
Live spot, rate, dividend yield |
| GET | /api/v1/market/expiries |
Listed option expirations |
| GET | /api/v1/market/chain |
Option chain + implied vols |
| GET | /api/v1/market/vol_surface |
Market-calibrated IV surface |
| WS | /ws/pricing |
Real-time PDE pricing over WebSocket |
Phase 5 — Real-time dashboard
From a repo clone (full live-reload experience, Node.js required):
# One command — starts FastAPI :8000 + Next.js :3000, opens browser
nabla dashboard
# NablaQuant — Full-Stack Dashboard
# Backend (FastAPI + WS) : http://localhost:8000
# Frontend (Next.js) : http://localhost:3000 ← dashboard opens here
#
# Press Ctrl-C to stop both servers.
npm install runs automatically the first time. NEXT_PUBLIC_WS_URL and NEXT_PUBLIC_API_URL are injected by the CLI so the frontend always connects to the right backend port.
From a PyPI install (no Node.js needed — bundled static export):
nabla serve
# → http://localhost:8000 dashboard + API on one port
# → http://localhost:8000/docs interactive Swagger UI
Rebuild bundled dashboard after frontend changes:
cd dashboard && npm run build
python -c "
import shutil, pathlib
shutil.copytree('out', '../python/nabla_quant/dashboard_static', dirs_exist_ok=True)
"
Phase 5 — Low-level C++ bindings
from nabla_quant import _nabla_core as core
mesh = core.SinhMesh(strike=100, s_min=0, s_max=400,
num_nodes=801, concentration=12.5)
bc = core.BoundaryConditions(
mesh,
core.MarketParams(rate=0.05, vol=0.20, dividend=0.0))
arena = core.Arena(mesh)
lv = core.LocalVolSurface.flat(0.20)
solver = core.CrankNicolsonSolver(mesh, bc, arena, lv)
solver.solve(num_time_steps=800, theta=0.5, expiry=1.0,
option_type=core.OptionType.Call)
import numpy as np
nodes = mesh.spot_nodes # np.ndarray of spot values
values = solver.solution # np.ndarray of option prices at t = 0
Running the full test suite
# C++ (275+ assertions across 14 test suites)
cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel
cd build && ctest --output-on-failure
# Python SDK + API (106 tests, ~2 s)
PYTHONPATH=python python -m pytest python/tests/test_sdk.py python/tests/test_api.py -v
# Real-world market data tests (requires internet)
PYTHONPATH=python python -m pytest python/tests/test_real_world.py -v
# Full 16-contract pipeline stress test with PDE/Market ratios
PYTHONPATH=python python python/tests/stress_pipeline.py
Phase 1: Mathematical Foundation & Discretization
Phase 1 maps the continuous Black-Scholes PDE to a discrete computational domain, optimized for both accuracy and performance.
Non-Uniform Mesh Generation
Standard uniform grids waste compute: they over-sample the deep ITM/OTM tails where the option value is nearly linear, while under-sampling the critical region near the strike where curvature (Gamma) is highest.
We implement a hyperbolic sine coordinate transformation that solves this problem:
Forward transform (computational → physical):
S(ξ) = K + c · sinh(ξ)
Inverse transform:
ξ(S) = arcsinh((S − K) / c)
Jacobian:
dS/dξ = c · cosh(ξ)
Where:
- K is the strike price (clustering center)
- c is the concentration parameter (smaller c → tighter clustering)
- ξ lives on a uniform computational grid
The Jacobian c · cosh(ξ) is minimized at ξ = 0 (i.e., S = K), so uniform ξ-spacing produces the smallest physical spacing precisely where we need the most resolution. The cosh function grows exponentially in the tails, naturally sparsifying the far-field grid.
Concentration parameter effect (N = 201, domain [0, 300], K = 100):
| c | Min Δs | Max Δs | Ratio |
|---|---|---|---|
| 2 | 0.099 | 9.663 | 97.5 |
| 10 | 0.334 | 6.586 | 19.7 |
| 20 | 0.531 | 5.267 | 9.9 |
| 100 | 1.163 | 2.586 | 2.2 |
| 500 | 1.472 | 1.584 | 1.1 |
Boundary Conditions
The PDE domain requires three conditions:
Terminal condition (t = T):
Call: V(S, T) = max(S − K, 0)
Put: V(S, T) = max(K − S, 0)
Lower spatial boundary (S → 0):
Call: V(0, t) = 0
Put: V(0, t) = K · exp(−r · τ) − S_min · exp(−q · τ)
Upper spatial boundary (S → S_max):
Call: V(S_max, t) = S_max · exp(−q · τ) − K · exp(−r · τ)
Put: V(S_max, t) = 0
Where τ = T − t is the time to maturity, r is the risk-free rate, and q is the continuous dividend yield.
Phase 2: Core Engine & Bare-Metal Optimization
The solver is built in pure, dependency-free C++17, shifting the bottleneck from algorithmic complexity to hardware execution limits.
Crank-Nicolson Scheme
The Black-Scholes PDE is discretized using the unconditionally stable θ-scheme (θ = 0.5):
∂V/∂τ = ½σ²S² ∂²V/∂S² + (r-q)S ∂V/∂S - rV
Second-order accurate finite differences on the non-uniform SinhMesh produce a tridiagonal system at each time step. Both first and second spatial derivatives use the full non-uniform stencil for O(h²) accuracy.
Thomas Algorithm - O(N)
A custom tridiagonal solver advances the grid backward in time without any matrix inversion libraries. Forward elimination followed by back substitution in O(N) with pre-allocated scratch workspace.
Memory Arena Allocator
All solver workspace (coefficient arrays, tridiagonal system, volatility vectors) is pre-allocated from a single 64-byte aligned contiguous memory block at construction time. The hot time-stepping loop performs zero heap allocations, guaranteeing:
- Cache-local memory access patterns
- No fragmentation or
malloclatency - Deterministic execution time
SIMD Vectorization (AVX2)
The innermost loops, PDE coefficient computation and RHS assembly, are accelerated with AVX2 intrinsics, processing 4 doubles per cycle (256-bit registers) with FMA (fused multiply-add) support. Automatic scalar fallback on non-AVX2 platforms.
Local Volatility Surfaces
The engine supports σ(S, t) via the LocalVolSurface class:
- Flat: constant volatility (reduces to standard BS)
- CEV: σ(S) = σ₀(S/S₀)^(β−1) for leverage effects
- Skew: σ(S,t) = σ_atm + skew·ln(S/K) + term·√t
- Custom: arbitrary user-defined std::function
When local vol is active, PDE coefficients are recomputed at each time step at the midpoint time.
American Option Early Exercise
Early-exercise boundary conditions are handled via the projection method: after each Crank-Nicolson step, the solution is projected onto the constraint V(S) ≥ payoff(S). The payoff vector is pre-computed in the arena for zero-allocation enforcement.
Phase 3: Sensitivities & Adjoint Algorithmic Differentiation (AAD)
Calculating risk sensitivities (the Greeks) efficiently is often more critical than the price itself. Phase 3 implements three complementary methods.
Direct Spatial Greeks: O(1) Extra Cost
Delta (Δ) and Gamma (Γ) are computed directly from the solved grid using second-order non-uniform finite differences. No additional PDE solves required.
Delta (non-uniform central difference):
Δ_i = [h_m² V_{i+1} + (h_p² − h_m²) V_i − h_p² V_{i-1}] / [h_m h_p (h_m + h_p)]
Gamma (non-uniform second derivative):
Γ_i = 2/(h_m + h_p) · [(V_{i+1} − V_i)/h_p − (V_i − V_{i-1})/h_m]
Theta from the Black-Scholes PDE identity (exact, no extra computation):
Θ = rV − (r−q)SΔ − ½σ²S²Γ
Adjoint Algorithmic Differentiation (AAD)
Standard "bumping" (re-running the PDE with perturbed parameters) requires O(P) full PDE solves for P parameters. AAD replaces this with a single backward sweep over the Crank-Nicolson computational graph, computing all parametric sensitivities simultaneously in O(N_t · N_S), the same cost as one forward solve.
The adjoint equation at each backward time step:
μ = (I − θΔt L)⁻ᵀ λ # Adjoint Thomas solve (swap lower ↔ upper)
λ ← (I + (1−θ)Δt L)ᵀ μ # Propagate through explicit part
∂f/∂p += μᵀ · Δt · (∂L/∂p) · W # Accumulate parameter sensitivity
where W = (1−θ)V^n + θV^{n+1} is the Crank-Nicolson weighted average, and ∂L/∂p is the sensitivity of the spatial operator to parameter p.
Computed via AAD:
- Vega (ν) = ∂V/∂σ - sensitivity to volatility
- Rho (ρ) = ∂V/∂r - sensitivity to interest rate (includes boundary value sensitivity)
Accuracy vs Black-Scholes Analytical (ATM Call, S=K=100, T=1, r=5%, σ=20%)
| Greek | Analytical | AAD Engine | Error |
|---|---|---|---|
| Price | 10.4506 | 10.4506 | 2.4e-5 |
| Delta | 0.6368 | 0.6368 | -8e-6 |
| Gamma | 0.01876 | 0.01876 | -3e-7 |
| Theta | -6.4140 | -6.4140 | 7.2e-5 |
| Vega | 37.524 | 37.523 | -6.2e-4 |
| Rho | 53.232 | 53.232 | -3.8e-4 |
Bump-and-Revalue Reference
A full bump-and-revalue implementation is provided for independent validation: central differences in σ and r (4 re-solves), grid interpolation for Δ/Γ, forward difference for Θ. The AAD and bump results agree to high precision across all test cases.
Phase 4: Barrier Options & Exotic Pricing
Barrier options enforce absorbing boundary conditions within the PDE time-stepping loop, dynamically zeroing (or rebating) the grid when the asset price breaches a predefined barrier.
Barrier Types
All 8 standard European barrier types are supported:
| Direction | Knock | Description |
|---|---|---|
| Down | Out | Killed if S ever touches B (B < S) |
| Down | In | Activates only if S touches B |
| Up | Out | Killed if S ever touches B (B > S) |
| Up | In | Activates only if S touches B |
Each type works with both calls and puts, giving 8 total combinations.
Knock-Out Implementation
After each Crank-Nicolson time step, the absorbing barrier is enforced:
for each node i:
if direction == Down and S[i] <= B: V[i] = rebate * e^{-rτ}
if direction == Up and S[i] >= B: V[i] = rebate * e^{-rτ}
The implicit boundary contributions in the tridiagonal RHS are also corrected: domain boundaries beyond the barrier use the barrier value (rebate) instead of the vanilla Dirichlet conditions, preventing value leakage through the tridiagonal coupling.
Knock-In via In-Out Parity
Knock-in options are computed without a separate PDE solve:
V_knock_in = V_vanilla − V_knock_out
This identity is exact and verified to hold to 6+ decimal places in the PDE.
Analytical Validation
Rubinstein-Reiner (1991) closed-form formulas for DOC, UOC, DOP, and UOP under constant volatility are provided for test validation. The PDE converges to these analytical values with grid refinement:
| Grid | DOC Price | Error vs Analytical |
|---|---|---|
| 101×100 | 10.400 | 0.049 |
| 201×200 | 10.383 | 0.032 |
| 401×400 | 10.371 | 0.020 |
| 801×800 | 10.364 | 0.012 |
| 1601×1600 | 10.359 | 0.008 |
Features
- Rebate support: optional cash rebate paid on knock-out
- Local volatility: barriers work with CEV, skew, and custom σ(S, t) surfaces
- Input validation: barrier level must be within mesh domain; up barriers must exceed spot; down barriers must be below spot
Phase 5: Python SDK & FastAPI Layer
The C++ engine is accessible from Python without sacrificing execution speed, enabling direct integration into ML/trading ecosystems.
Python SDK (nabla_quant)
The pybind11 bindings expose the full C++ API as a native Python module with zero-copy numpy array returns.
High-level convenience API:
import nabla_quant as nq
# European option
result = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20)
print(result.price) # 10.4506
# All Greeks via AAD - single backward sweep
g = nq.greeks(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20)
print(f"Δ={g.delta:.4f} Γ={g.gamma:.5f} Θ={g.theta:.4f} ν={g.vega:.3f} ρ={g.rho:.3f}")
# American option with early exercise
result = nq.american(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20, option_type="put")
# Barrier option (all 8 types supported)
result = nq.barrier(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
level=80, direction="down", knock="out")
# Local volatility (CEV model)
result = nq.european(spot=100, strike=100, expiry=1.0, rate=0.05, vol=0.20,
vol_model="cev", vol_params={"beta": 0.5})
Low-level access (direct C++ bindings):
from nabla_quant import SinhMesh, BoundaryConditions, CrankNicolsonSolver, LocalVolSurface
mesh = SinhMesh(strike=100, s_min=0, s_max=400, num_nodes=801, concentration=12.5)
# mesh.spot_nodes → numpy array, mesh.spacing_ratio, mesh.strike_index, etc.
FastAPI Pricing API
An async REST API dispatches pricing requests to the C++ engine via a thread pool, keeping the event loop free for I/O.
Endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/health |
Engine status and version |
| POST | /api/v1/price/european |
European option pricing |
| POST | /api/v1/price/american |
American option pricing |
| POST | /api/v1/price/barrier |
Barrier option pricing |
| POST | /api/v1/greeks |
Full Greeks via AAD |
| POST | /api/v1/price/local_vol |
Local volatility pricing |
| POST | /api/v1/price/batch |
Batch pricing (up to 100 requests) |
Running the API server:
cd python && PYTHONPATH=. uvicorn nabla_quant.api.app:app --host 0.0.0.0 --port 8000
Example request:
curl -X POST http://localhost:8000/api/v1/greeks \
-H "Content-Type: application/json" \
-d '{"spot": 100, "strike": 100, "expiry": 1.0, "rate": 0.05, "volatility": 0.20}'
Response:
{"price": 10.4506, "delta": 0.6368, "gamma": 0.01876, "theta": -6.414, "vega": 37.524, "rho": 53.232}
All request parameters are validated via Pydantic: spot/strike/vol must be positive, rate in [-0.5, 2.0], grid size bounded [51, 10001], etc. Invalid requests return 422 with descriptive errors.
Building
Requirements: CMake ≥ 3.20, C++17 compiler with AVX2 support (GCC ≥ 7, Clang ≥ 5, or MSVC ≥ 2017), Python ≥ 3.8 with pybind11
cmake -B build -DCMAKE_BUILD_TYPE=Release -DNABLA_BUILD_PYTHON=ON
cmake --build build
To build without Python bindings:
cmake -B build -DCMAKE_BUILD_TYPE=Release -DNABLA_BUILD_PYTHON=OFF
cmake --build build
Running Tests
C++ tests:
cd build && ctest --output-on-failure
Python tests (SDK + API + market + real-world ≈ 119 tests):
cd python && PYTHONPATH=. python -m pytest tests/ -v
C++ test summary: 14 test suites:
- MeshTests (22): construction, monotonicity, clustering, transform invertibility, Jacobian verification
- BoundaryTests (21): payoff, terminal condition, Dirichlet boundaries, put-call parity
- IntegrationTests (19): mesh+boundary combined, refinement, Greeks pipeline, convergence, edge cases
- ThomasTests (13): tridiagonal solver correctness, residual checks, symmetry, M-matrix positivity
- SolverTests (15): Crank-Nicolson vs Black-Scholes analytical (ATM/ITM/OTM, put-call parity, convergence)
- ArenaTests (14): allocation, alignment, overflow, move semantics, PDE loop simulation
- SIMDTests (9): AVX2 vs scalar exact match, dispatch, solver integration
- LocalVolTests (13): flat/CEV/skew models, leverage effect, grid non-negativity
- AmericanTests (11): early exercise premium, intrinsic floor, deep-ITM, BAW benchmark, CEV compatibility
- GreeksTests (18): spatial Delta/Gamma/Theta vs BS analytical, surface properties, dividends
- GreeksAADTests (18): AAD Vega/Rho vs BS analytical, AAD vs bump-and-revalue cross-validation
- GreeksValidationTests (32): deep OTM/ITM limits, zero rate, high/low vol, dividends, near-expiry, convergence, constructor validation, AAD stability
- BarrierTests (28): DOC/UOC/DOP/UOP vs Rubinstein-Reiner analytical, in-out parity (all 8 types), rebate, local vol, convergence, edge cases, input validation
Running the Demos
./build/examples/phase1_demo # Mesh & boundary visualization
./build/examples/phase2_demo # Full pricing engine demonstration
./build/examples/phase3_demo # Greeks & AAD comparison vs analytical
./build/examples/phase4_demo # Barrier options: all 8 types, parity, local vol, convergence
Project Structure
NablaQuant/
├── CMakeLists.txt
├── include/nabla/
│ ├── nabla.hpp # Top-level include
│ └── core/
│ ├── types.hpp # MarketParams, ContractParams, ExerciseType
│ ├── mesh.hpp # SinhMesh: non-uniform grid generation
│ ├── boundary.hpp # BoundaryConditions: terminal + Dirichlet
│ ├── thomas.hpp # ThomasSolver: O(N) tridiagonal solver
│ ├── arena.hpp # Arena: zero-allocation memory pool
│ ├── simd.hpp # AVX2-vectorized PDE operations
│ ├── local_vol.hpp # LocalVolSurface: σ(S, t) models
│ ├── solver.hpp # CrankNicolsonSolver: PDE engine
│ ├── greeks.hpp # GreeksCalculator: AAD + spatial Greeks
│ └── barrier.hpp # BarrierPricer: knock-out/knock-in options
├── src/core/
│ ├── mesh.cpp, boundary.cpp, thomas.cpp, solver.cpp
│ ├── arena.cpp, simd.cpp, local_vol.cpp
│ ├── greeks.cpp # AAD backward sweep + spatial Greeks
│ └── barrier.cpp # Barrier option PDE + analytical formulas
├── tests/
│ ├── test_harness.hpp
│ ├── test_mesh.cpp, test_boundary.cpp, test_integration.cpp
│ ├── test_thomas.cpp, test_solver.cpp, test_arena.cpp
│ ├── test_simd.cpp, test_local_vol.cpp, test_american.cpp
│ ├── test_greeks.cpp # Spatial Greeks vs BS analytical
│ ├── test_greeks_aad.cpp # AAD vs BS + AAD vs bump-and-revalue
│ ├── test_greeks_validation.cpp # 32 rigorous edge/exceptional case tests
│ └── test_barrier.cpp # 34 barrier option tests + parity checks
├── examples/
│ ├── phase1_demo.cpp
│ ├── phase2_demo.cpp
│ ├── phase3_demo.cpp # Greeks & AAD demonstration
│ └── phase4_demo.cpp # Barrier options demonstration
├── python/
│ ├── CMakeLists.txt # pybind11 module build
│ ├── bindings.cpp # C++ ↔ Python bindings
│ ├── requirements.txt
│ ├── nabla_quant/
│ │ ├── __init__.py # High-level Pythonic API
│ │ └── api/
│ │ ├── __init__.py
│ │ ├── app.py # FastAPI + WebSocket endpoints
│ │ └── models.py # Pydantic request/response models
│ └── tests/
│ ├── conftest.py
│ ├── test_sdk.py # 76 SDK validation tests
│ ├── test_api.py # 30 API endpoint tests
│ └── test_real_world.py # 12 market-realistic validation tests
└── dashboard/
├── package.json # Next.js 16 + react-plotly.js
├── src/
│ ├── app/
│ │ ├── layout.tsx # Root layout with dark theme
│ │ ├── page.tsx # Main dashboard page
│ │ └── globals.css # Quant-terminal dark theme
│ ├── components/
│ │ ├── Header.tsx # NablaQuant header bar
│ │ ├── ParameterPanel.tsx # Interactive parameter sliders
│ │ ├── PricingCard.tsx # Live Greeks display (Δ, Γ, Θ, ν, ρ)
│ │ ├── SurfacePlot.tsx # 3D option value surface V(S, T)
│ │ ├── VolSurfacePlot.tsx # 3D implied vol surface σ(S, T)
│ │ ├── GreeksHeatmap.tsx # Selectable Greeks heatmap
│ │ ├── PlotlyChart.tsx # Dynamic Plotly import (SSR-safe)
│ │ └── StatusBar.tsx # Connection + compute time status
│ └── lib/
│ ├── types.ts # TypeScript interfaces
│ └── useNablaSocket.ts # WebSocket hook with auto-reconnect
└── .env.local # WS_URL configuration
Phase 5 (continued): Real-Time Visualization Dashboard
Architecture
┌──────────────────┐ WebSocket ┌─────────────────┐ pybind11 ┌──────────────┐
│ Next.js + Plotly │ ←─────────────────→ │ FastAPI + WS │ ←─────────────→ │ C++ PDE Core │
│ (React frontend) │ /ws/pricing │ (Python SDK) │ _nabla_core │ (CN + AAD) │
└──────────────────┘ └─────────────────┘ └──────────────┘
Dashboard Features
| Panel | Description |
|---|---|
| Parameter Sliders | S, K, T, r, σ, q: instant recalc on change (250ms debounce) |
| Greeks Card | Live Δ, Γ, Θ, ν, ρ with color-coded sign indicators |
| Option Value Surface | 3D Plotly surface V(S, T): blue colorscale, rotatable |
| Volatility Surface | 3D implied vol σ(S, T) with skew + term structure: purple colorscale |
| Greeks Heatmap | 2D heatmap of any Greek over (S, T) with selectable tabs |
| Status Bar | WebSocket connection status, compute latency |
Running the Dashboard
From repo clone (live Next.js, recommended):
nabla dashboard # starts FastAPI :8000 + Next.js :3000, opens browser
From PyPI install (bundled static, no Node.js):
nabla serve # http://localhost:8000
Manual (two terminals):
cd python && uvicorn nabla_quant.api.app:app --host 0.0.0.0 --port 8000
cd dashboard && npm install && npm run dev # http://localhost:3000
The dashboard works in two modes:
- Online (WebSocket): Connects to the FastAPI backend for C++ PDE pricing
- Offline (analytical): Falls back to Black-Scholes formulas computed in the browser
WebSocket Protocol
The /ws/pricing endpoint accepts JSON messages:
{
"type": "single_price | surface | greeks_surface | vol_surface",
"params": { "spot": 100, "strike": 100, "expiry": 1.0, "rate": 0.05, "volatility": 0.2 },
"spot_range": [50, 150],
"expiry_range": [0.05, 1.0],
"spot_steps": 30,
"expiry_steps": 20
}
Response includes compute time in milliseconds for performance monitoring. Optional request_id is echoed so the dashboard can ignore out-of-order replies.
Delayed market data (yfinance)
Optional endpoints for demos (not production trading data):
GET /api/v1/market/quote?symbol=SPY- last close as spot referenceGET /api/v1/market/expiries?symbol=SPY- listed option expirationsGET /api/v1/market/chain?symbol=SPY&expiration=YYYY-MM-DD- near-ATM calls/puts with implied vol
Requires pip install yfinance (listed in python/requirements.txt). The dashboard sidebar includes Real-World Options (Yahoo) to pull a quote, load a chain, and apply strike / IV / expiry to the sliders.
Copy dashboard/.env.example to dashboard/.env.local and set NEXT_PUBLIC_API_URL if the API is not on localhost:8000.
Pipeline Stress Test
Full end-to-end validation across 16 option contracts: standard, high-value/institutional, live-market-calibrated, exotic, and invalid inputs. Run with:
cd <repo root>
PYTHONPATH=python python python/tests/stress_pipeline.py
Test Matrix
| # | Contract | Type | S | K | T (yr) | r% | vol% | q% | PDE $ | BS $ | Err% | Notes |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | ATM EUR call | European | 100 | 100 | 1.000 | 5.00 | 20.0 | 0.00 | 10.4505 | 10.4506 | < 0.01% | Benchmark case |
| 2 | OTM EUR put | European | 100 | 110 | 0.500 | 5.00 | 20.0 | 0.00 | 10.1905 | 10.1906 | < 0.01% | 10% OTM |
| 3 | ITM EUR call | European | 110 | 100 | 1.000 | 5.00 | 20.0 | 0.00 | 17.6629 | 17.6630 | < 0.01% | 10% ITM |
| 4 | 1-week ATM call | European | 100 | 100 | 0.020 | 5.00 | 25.0 | 0.00 | 1.4602 | 1.4602 | < 0.01% | Short-dated |
| 5 | 2yr div call | European | 100 | 100 | 2.000 | 5.00 | 20.0 | 4.00 | 11.2183 | 11.2183 | < 0.01% | High continuous div |
| 6 | Deep-ITM AM put | American | 80 | 100 | 1.000 | 5.00 | 25.0 | 0.00 | 20.3633 | 18.2645 (EU) | +11.5% (AM) | Early-exercise premium $2.10 |
| 7 | SPX-scale ATM | European | 5300 | 5300 | 0.250 | 4.50 | 16.0 | 1.40 | 189.037 | 189.038 | < 0.01% | Institutional scale |
| 8 | SPY live ATM | European | 679.5 | 679 | 0.011 | 3.59 | 13.9 | 0.83 | 4.2808 | 4.2806 | 0.01% | Live: mkt mid $4.62 |
| 9 | AAPL live ATM | European | 260.5 | 260 | 0.016 | 3.59 | 24.0 | 0.40 | 3.5105 | 3.5106 | < 0.01% | Live: mkt mid $3.63 |
| 10 | MSFT live ATM | European | 370.9 | 370 | 0.016 | 3.59 | 26.4 | 0.93 | 5.5278 | 5.5276 | < 0.01% | Live: mkt mid $5.83 |
| 11 | DOC barrier | Barrier (DOC) | 100 | 100 | 1.000 | 5.00 | 20.0 | 0.00 | 10.3605 | 10.4506 (van) | — | Barrier B=80, knock-out reduces value |
| 12 | UOC barrier | Barrier (UOC) | 100 | 100 | 1.000 | 5.00 | 20.0 | 0.00 | 3.3915 | 10.4506 (van) | — | Barrier B=130, strong knock-out effect |
| 13 | CEV local vol | LV European | 100 | 100 | 1.000 | 5.00 | 20.0 | 0.00 | 10.4538 | 10.4506 | 0.03% | β=0.5, leverage effect |
| 14 | Crisis vol | European | 100 | 100 | 0.500 | 2.00 | 80.0 | 0.00 | 22.6602 | 22.6603 | < 0.01% | σ=80% stress scenario |
| 15 | 3-day near-expiry | European | 100 | 101 | 0.008 | 5.00 | 20.0 | 0.00 | 0.3460 | 0.3459 | 0.03% | High gamma near expiry |
| 16 | INVALID: spot < 0 | — | −10 | 100 | 1.000 | — | — | — | — | — | — | API: HTTP 422 |
Dates and live-market prices are sourced from Yahoo Finance at the time of the test run (Apr 2026). Strikes and IVs reflect the nearest-ATM contract on the next available expiration.
Greeks: AAD vs Black-Scholes Analytical (Case #1 - ATM Call)
| Greek | Analytical | PDE/AAD | Abs Error |
|---|---|---|---|
| Price | 10.4506 | 10.4505 | 1.0e-4 |
| Delta (Δ) | 0.6368 | 0.6368 | < 1e-4 |
| Gamma (Γ) | 0.01876 | 0.01876 | < 1e-5 |
| Theta (Θ) | −6.414 | −6.414 | < 1e-3 |
| Vega (ν) | 37.524 | 37.524 | < 0.001 |
Live Market Calibration
PDE prices use constant-volatility Black-Scholes. Discrepancy vs mid-market arises from discrete dividends, volatility skew, bid-ask spread, and microstructure effects, all outside the constant-vol model by design.
| Ticker | Spot | Strike | T (days) | IV | r | q | PDE $ | Mkt mid $ | PDE/Mkt |
|---|---|---|---|---|---|---|---|---|---|
| SPY | 679.46 | 679 | 4 | 13.9% | 3.59% | 0.83% | 4.2808 | 4.6150 | 0.928 |
| AAPL | 260.48 | 260 | 6 | 24.0% | 3.59% | 0.40% | 3.5105 | 3.6250 | 0.968 |
| MSFT | 370.87 | 370 | 6 | 26.4% | 3.59% | 0.93% | 5.5278 | 5.8250 | 0.949 |
PDE/Market of 0.93–0.97 for short-dated ATM options is expected for a constant-vol model. The gap closes when local-vol or stochastic-vol surfaces are plugged in via the LocalVolSurface interface.
Barrier & Structural Checks
| Check | Result | |
|---|---|---|
| In-out parity: DOC + DIC = vanilla | 10.3605 + 0.0901 = 10.4506 | error = 0.00e+00 ✓ |
| American early-exercise premium (deep ITM put) | AM $20.36 − EU $18.26 = $2.10 | premium > 0 ✓ |
| Put-call parity at SPX scale | C − P = $40.77 ; Se^(−qT) − Ke^(−rT) = $40.77 | error = 0.0000 ✓ |
API Input Validation (Pydantic / HTTP 422)
| Invalid Input | HTTP Status |
|---|---|
| vol > 500% | 422 Rejected |
| spot < 0 | 422 Rejected |
| strike = 0 | 422 Rejected |
| expiry = 0 | 422 Rejected |
Summary
| Metric | Result |
|---|---|
| Contracts tested | 16 |
| BS pricing error (European, avg) | < 0.005% |
| Live PDE/Market ratio range | 0.928 – 0.968 |
| In-out barrier parity error | 0.00e+00 |
| Put-call parity error (SPX scale) | 0.0000 |
| Invalid inputs rejected (API) | 4 / 4 |
| Total C++ compute time (16 cases) | < 750 ms |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nabla_quant-0.1.1.tar.gz.
File metadata
- Download URL: nabla_quant-0.1.1.tar.gz
- Upload date:
- Size: 1.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd1d1b2d6db5fabd58cbb6704796d12f3518706707bbc60e20ac8e4b9e40dfe9
|
|
| MD5 |
f3aba4b69043e6183a5615830f04f61e
|
|
| BLAKE2b-256 |
d0a7ed0490f5d03ee26553f2703a742c3aafbfa492b3fab9f26b732d2d0ae33c
|
File details
Details for the file nabla_quant-0.1.1-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: nabla_quant-0.1.1-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 2.3 MB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0363ce38467ea636abd1d6c81d4a6fc404a8333dedd0ea4cf93182c791e16e8e
|
|
| MD5 |
8e261b9cb61b79a15793c5c84b056e97
|
|
| BLAKE2b-256 |
270eb4182143338dc478214657a45639162328bac959a7d75b30827f1f199dad
|