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 |
Metadata
Release files for nabla-quant 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nabla_quant-0.1.1.tar.gz | 1.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nabla_quant-0.1.1-cp312-cp312-win_amd64.whl | CPython 3.12 | CPython 3.12 | Windows x86-64 | Details |
Total release size: 4.2 MB
Release files / nabla_quant-0.1.1.tar.gz
| Download URL | nabla_quant-0.1.1.tar.gz |
|---|---|
| Size | 1.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dd1d1b2d6db5fabd58cbb6704796d12f3518706707bbc60e20ac8e4b9e40dfe9
|
|
BLAKE2b-256 checksum How to use checksums |
d0a7ed0490f5d03ee26553f2703a742c3aafbfa492b3fab9f26b732d2d0ae33c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.4
|
Release files / nabla_quant-0.1.1-cp312-cp312-win_amd64.whl
| Download URL | nabla_quant-0.1.1-cp312-cp312-win_amd64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.12 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
0363ce38467ea636abd1d6c81d4a6fc404a8333dedd0ea4cf93182c791e16e8e
|
|
BLAKE2b-256 checksum How to use checksums |
270eb4182143338dc478214657a45639162328bac959a7d75b30827f1f199dad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.4
|