▶ Live explorer: aleks-drozy.github.io/options-pricing-engine (sliders for S, K, sigma, T, r, q; live BS/tree/MC prices, payoff + Greeks, and a self-check badge that re-verifies its own numbers on every load).
Implied vol inverted from real SPY call quotes: on the left, the smile against
moneyness K/S for the four nearest expiries (9, 10, 11 and 13 DTE): the
market quotes a different sigma at every strike, which flat-vol
Black-Scholes says is impossible; on the right, at-the-money IV by expiry
across the snapshot's 9-90 DTE window (13.6% to 15.9%).
Three pricers, one truth, and the market disagrees with all of them.
Black-Scholes closed form, a CRR binomial tree (European and American), and a
seeded Monte Carlo simulation all price the same vanilla option. Seven
machine-checked gates prove the three agree with each other to within their
own numerical tolerances. Then the engine turns on a real SPY option chain and
inverts the market's own prices back to volatility, and the market quotes a
different sigma for every strike. Flat-vol Black-Scholes can't explain that;
the smile is the market pricing in the fat tails and skew that a lognormal
model assumes away. See WRITEUP.md for the full finding.
| # | Gate | Result |
|---|---|---|
| 1 | Put-call parity (BS closed form) | ✓ worst gap 2.84e-14 (< 1e-10) |
| 2 | Put-call parity (Monte Carlo) | ✓ 16/16 grid points, 0 failures |
| 3 | Tree → BS convergence | ✓ 0 failures; averaged error shrinks N=200 → N=2000 |
| 4 | MC → BS convergence | ✓ error 0.00858 vs SE 0.01472 at N=1e6; 95.0% CI coverage over 200 seeds |
| 5 | Greeks (closed form vs FD) | ✓ worst rel. error 5.57e-6; American tree Greeks sane |
| 6 | No-arbitrage (American ≥ European) | ✓ 0 violations, worst q=0 gap 4.55e-13 |
| 7 | IV round-trip (synthetic + real SPY snapshot) | ✓ synthetic worst error 8.42e-13; 3587 kept quotes, 3485 resolved (97.2%) + 102 counted failures |
Gate 4 in one picture: absolute |MC - BS| error for a single seed from 1e3 to
1e6 paths, plotted against the simulation's own standard error and an
O(1/sqrt(N)) reference. A single seed bounces around that line, which is why
the gate scores coverage over 200 seeds rather than one run.
Real numbers, regenerated from results/validation.json
by run_validate.py, not hand-typed. Three gate criteria have been amended
after gate runs themselves surfaced numerical edge cases; see
WRITEUP.md for all three, with rationale.
This is a research project, not trading advice.
Three independent pricers that cross-check each other, 7 machine-checked invariant gates, property-tested with Hypothesis, exposed to any MCP agent so that every price arrives with its own proof.
Options pricing MCP servers are not a new idea: a GitHub code-search for
options pricing mcp server returns 13 repos today, including
quantoracle (63 tools,
Greeks, IV, barrier/Asian/lookback, multi-leg P&L, its own accuracy-benchmark
suite), mcp-optionsflow (36
★, the most-starred entry), 78degrees/mcp-server
(portfolio risk + stress testing), and
quant-options-engine
(near-identical "pricing engine + MCP server" framing). Portfolio Greeks
aggregation and shock-grid stress testing both already exist elsewhere.
Building either as "the novel part" would land as a fourteenth entry in an
occupied search result.
What every one of those servers returns is a number, with no indication of
how much to trust it: benchmarks live in CI, not in the response. That
matters specifically because the consumer is an LLM, which cannot check the
arithmetic it's handed and will confidently relay a wrong number. So instead
of a CI badge computed once and forgotten, every tool call here attaches a
per-call verification receipt, computed at the caller's exact
parameters: put-call parity residual, no-arbitrage bounds, and (at
verify="full") cross-method agreement between Black-Scholes, the CRR
tree, and Monte Carlo, or american_ge_european for early-exercise
contracts. The agent is told how much to trust the number it just received,
not just handed the number.
mcp_server/
├── server.py # MCPServer + 5 tools: price_option, compute_greeks,
│ # implied_vol, scenario_analysis, portfolio_greeks
├── verify.py # the differentiator: per-call Receipt (see below)
├── portfolio.py # Greeks aggregation, grouped by underlying
├── scenario.py # shock-grid P&L repricing, clamped at expiry
├── schemas.py # pydantic in/out models mirroring validate_inputs
├── units.py # Greek unit conversion (raw vs conventional)
└── __main__.py # python -m mcp_server entry point
A receipt that has never caught anything is decoration.
tests/test_verify.py monkeypatches the pricers with
deliberately broken implementations (dropping the e^-qT dividend discount,
doubling the tree price, halving the American premium below the European
one) and asserts the receipt actually flips to all_passed: false: proof
the checks do something, not just report a static "PASS".
A note on unit conversion: the engine returns vega per 1.00 vol and theta
per year. An LLM asked to relay those numbers will, by default, assume
vega-per-vol-point and theta-per-day and misreport by 100x/365x.
compute_greeks returns both conventions, labelled, so the model never has
to guess.
pip install -r requirements-mcp.txt # adds mcp, pydantic, hypothesis on top of requirements-dev.txt
python -m pytest tests/test_verify.py tests/test_mcp_schemas.py tests/test_mcp_tools.py \
tests/test_mcp_protocol.py tests/test_portfolio.py tests/test_scenario.py \
tests/test_properties.py # 102 tests, no network
python -m mcp_server # runs the server over stdioAdd to Claude Desktop's claude_desktop_config.json (find it via Settings →
Developer → Edit Config):
{
"mcpServers": {
"options-pricing": {
"command": "python",
"args": ["-m", "mcp_server"],
"cwd": "/absolute/path/to/options-pricing-engine"
}
}
}Restart Claude Desktop, then try: "Use price_option to price a SPY call,
S=450 K=460 T=0.05 r=0.05 sigma=0.15, with verify='full', and show me the
verification block." Or build the demo from the write-up: invert four real
SPY quotes to implied vol with implied_vol (four different sigmas: the
smile is real), assemble them into portfolio_greeks, stress it with
scenario_analysis, and expand the verification receipt on any response
(parity residual ~1e-14, cross-method spread, MC standard error) while a
plain wrapper would have handed back a bare float.
pip install -r requirements-dev.txt
python -m pytest # 59 tests, no network (+ 7 skipped)
python run_validate.py # -> results/validation.json (the 7-gate verdict above)
python -m scripts.make_charts # -> charts/*.png
python -m scripts.build_viz # -> docs/index.html (the live explorer)Run these as modules (python -m scripts.x), not as scripts
(python scripts/x.py): the scripts import from the repo root
(engine, market, validation), which only resolves correctly when
Python's working directory is on the path as a package, i.e. via -m.
data/spy_chain.json is a committed snapshot; scripts/fetch_chain.py (which
refreshes it via yfinance) is run manually and never touched by CI or tests.
options-pricing-engine/
├── engine/
│ ├── bs.py # bs_price(S,K,T,r,sigma,q,kind) + closed-form greeks
│ ├── binomial.py # crr_price(..., steps, american: bool) + FD greeks
│ ├── montecarlo.py # mc_price(..., n_paths, seed) -> (price, std_error)
│ └── iv.py # implied_vol(price, S,K,T,r,q,kind) via brentq
├── market/
│ ├── snapshot.py # load data/spy_chain.json; quote-quality filter
│ └── smile.py # per-expiry IV smile + term structure tables
├── mcp_server/ # MCP delivery layer - engine/ above is untouched
│ ├── server.py # MCPServer + 5 tools (price/greeks/IV/scenario/portfolio)
│ ├── verify.py # per-call verification receipt (the differentiator)
│ ├── portfolio.py # Greeks aggregation, grouped by underlying
│ ├── scenario.py # shock-grid repricing, clamped at expiry
│ ├── schemas.py # pydantic in/out models
│ └── units.py # Greek unit conversion (raw vs conventional)
├── validation/
│ ├── parity.py # put-call parity gate
│ ├── convergence.py # tree->BS and MC->BS gates
│ ├── greeks_check.py# closed-form vs FD gate
│ └── noarb.py # American>=European; Am call == Eu call when q=0
├── scripts/
│ ├── fetch_chain.py # yfinance pull -> data/spy_chain.json (manual, not CI)
│ ├── make_charts.py # charts/*.png
│ ├── make_golden.py # viz golden table -> results/golden.json
│ └── build_viz.py # inject data+golden into viz/template.html -> docs/index.html
├── viz/template.html
├── tests/ # pytest, no network, 59 engine tests + 102 MCP tests
├── run_validate.py # runs all gates -> results/validation.json, non-zero exit on fail
├── data/spy_chain.json # committed SPY snapshot
├── results/ · charts/ · docs/ (docs/ = GitHub Pages, self-check PASS 24/24)
└── README.md · WRITEUP.md · requirements.txt · requirements-mcp.txt
· .github/workflows/tests.yml
- WRITEUP.md: method, the full 7-gate results table, the smile finding, and limitations
- docs/specs/2026-07-18-options-pricing-engine.md: the design spec, written before the engine was, including the amendments log

