Skip to main content

noarb

tests PyPI Python License

Point it at an option chain. It tells you which quotes cannot all be true at once, and which one to check first.

$ noarb --chain chain.csv --only-violations

SEVERITY   RULE                 EXPIRY      DETAIL                                                        REASON
violation  put-call-parity      2027-03-09  strike 110: call minus put is -4.80, parity requires -7.80    Parity is an identity for European options. Check for a dividend the feed omitted, then whether these are American.
violation  put-call-parity      2026-12-09  strike 100: call minus put is 3.49, parity requires 0.99      Parity is an identity for European options. Check for a dividend the feed omitted, then whether these are American.
violation  put-call-parity      2026-12-09  strike 120: call minus put is -16.51, parity requires -18.76  Parity is an identity for European options. Check for a dividend the feed omitted, then whether these are American.
...

36 quotes, 10 violations, 0 warnings, largest slack 3.0000

Exit code 1, so a pipeline can refuse to trade on a chain that disagrees with itself.

Install

pip install noarb

No runtime dependencies. Python 3.9 and up. It never touches the network, and there is a CI job that fails the build if anyone adds an import that could.

Why

A chain that violates one of these conditions is not a trading opportunity. It is a data problem, and it is a data problem you want to find before it reaches a model rather than after.

The conditions checked here are model-free. None of them assumes Black-Scholes or any other distribution for the underlying — they follow from the payoffs alone. If one is violated, a portfolio exists that costs nothing today and cannot lose money later. That is a much stronger statement than "the implied volatility looks a bit odd", and it is why a violation is worth stopping a pipeline for.

What actually causes them, in order of how often: a stale quote, the wrong spot, a dividend the feed did not mention, or the wrong expiry calendar. So every finding names the cheapest explanation to check first.

What it checks

Rule Condition
lower-bound An option is never worth less than its discounted intrinsic value
upper-bound A call is never worth more than the forward; a put never more than the discounted strike
strike-monotonicity Calls get cheaper as the strike rises; puts get dearer
vertical-spread A spread never costs more than the most it can pay
butterfly-convexity Value is convex in the strike, with strike-distance weights so uneven ladders are handled
put-call-parity C − P = S·e^(−qT) − K·e^(−rT), an identity for European options
calendar More time is never worth less — checked only where there is no dividend, because with one it genuinely can be

Plus warnings, which do not fail the build: crossed markets, quotes whose spread is more than half the offer, and contracts that have already expired.

Input

A CSV. Headers are matched case-insensitively and tolerate spaces and underscores.

right,strike,expiry,price,spot,rate,dividend
call,95,2026-12-09,7.53,100,0.05,0
put,95,2026-12-09,1.60,100,0.05,0

expiry takes an ISO date or a bare year fraction. Give bid and ask instead of price and the mid is used, with both retained so the quote-quality checks still work. Pass --percent-rates if your rate column says 5 rather than 0.05.

Usage

noarb --chain chain.csv                      # table, exit 1 if anything is violated
noarb --chain chain.csv --only-violations    # drop the warnings
noarb --chain chain.csv --format json        # for a downstream tool
noarb --chain chain.csv --format csv         # for a spreadsheet
noarb --chain chain.csv --tolerance 0.05     # in currency, default 0.01
noarb --chain chain.csv --as-of 2026-09-09   # valuation date for ISO expiries
noarb --chain chain.csv --quiet              # exit code only

Exit codes: 0 clean, 1 at least one violation, 2 the input could not be read. Warnings alone still exit 0.

As a library

from noarb import bs

bs.price("call", spot=42, strike=40, years=0.5, rate=0.10, vol=0.20)  # 4.7594
bs.greeks("call", 100, 100, 1.0, 0.05, 0.25).vega  # per vol point
bs.implied_vol("call", 8.5917, 100, 100, 1.0, 0.05)  # 0.15

implied_vol returns None rather than a number when the quote does not identify one. That is not an error path — a deep in-the-money put quoted at parity prices identically at 5% and at 80% volatility, so any number returned would be invented. See the note below.

Units, fixed at the point of calculation so two callers cannot disagree:

delta, gamma per 1.00 of spot
vega, rho per 1 percentage point
theta per calendar day, 365-day year

How it is checked

The model is not tested against itself. Every assertion is a published value, an identity that holds for any arbitrage-free model, or a numerical derivative of the code's own price function:

  • Hull's textbook values reproduce exactly
  • put–call parity holds to 3e-14 across the grid
  • all five Greeks agree with central differences to 1e-6
  • implied volatility round-trips to 2.5e-08
  • every arbitrage check is tested with a chain that breaks it and a chain that does not, because a check that always fires is worse than one that never does

CI runs the suite on Python 3.9 through 3.13 across Linux, macOS and Windows, asserts the exit codes from the command line, fails if a runtime dependency or a network import appears, and builds and installs the wheel into a clean virtualenv before running it.

The implied volatility of a quote that implies nothing

Far enough from the money, a quote sits on its intrinsic value to the last bit a float can hold. Every volatility from zero up to some large number reprices it equally well, and a root finder will happily hand back the smallest one.

A deep in-the-money put quoted at parity would then report 0.00% implied volatility — which a reader takes for a market view rather than for the absence of one. So a volatility only counts as implied here when the price is measurably sensitive to it. Otherwise the answer is n/a, and that is the honest one.

This was found by a test, not by inspection: the solver returned 0.0 for 231 of 648 grid points before the guard existed.

Scope

European options, Black-Scholes-Merton, continuous dividend yield. American early exercise is not modelled — the parity check will report violations on American chains, and it will be right that the identity does not hold, not right that there is free money. Nothing here is investment advice.

The checks tell you which quotes disagree. Sizing the resulting position is a separate job, and there is a workbook for it: The Options Greeks and Position Sizing Workbook — live Excel formulas for pricing, all five Greeks, a strike ladder that proves its own put–call parity, risk-budgeted position sizing that reprices at the stop rather than approximating with delta, and a multi-leg payoff builder. Its numbers are verified against this package.

Licence

MIT. See LICENSE.

Metadata

Release files for noarb 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for noarb 1.0.0
File Size Uploaded
noarb-1.0.0.tar.gz 30.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for noarb 1.0.0
File Interpreter ABI Platform
noarb-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 51.2 kB

Release files / noarb-1.0.0.tar.gz

Download URL noarb-1.0.0.tar.gz
Size 30.5 kB
Tags Source
SHA-256 checksum
How to use checksums
7dcea52875a1c0373715dca9f21e656d0fa6c2afcf6c74ae7948cfbddc40e50a
BLAKE2b-256 checksum
How to use checksums
a845127e5c8a821ce7ff450243e9bae401e8a671cb66d6afbb965a4bfc41379d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release files / noarb-1.0.0-py3-none-any.whl

Download URL noarb-1.0.0-py3-none-any.whl
Size 20.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c21cccfad8b1754935c8fac8d14f74f3a6f5ab7cce607ec03acb7300af69b768
BLAKE2b-256 checksum
How to use checksums
8d8a768efbf63abc643729840cf96853b23d9cf1223e9b71b01a16c085856b74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page