Skip to content

Repository files navigation

Options Pricing Engine

tests

▶ 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).

SPY implied volatility smile and ATM term structure

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.

Verdict: 7/7 gates PASS

# 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

Monte Carlo pricing error against path count, on log-log axes

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.

MCP server: every price arrives with its own proof

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.

Try it

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 stdio

Add 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.

Quick start

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.

Project structure

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

Further reading

About

Black-Scholes, CRR binomial and Monte Carlo option pricers cross-validated by seven machine-checked numerical gates, with a self-verifying live explorer

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages