Metadata-Version: 2.4
Name: moneyness
Version: 0.1.0
Summary: Options pricing, Greeks and implied volatility, validated against closed-form results.
Author: Daniyal
License-Expression: MIT
Project-URL: Homepage, https://github.com/DaniyalMlk/moneyness
Project-URL: Source, https://github.com/DaniyalMlk/moneyness
Project-URL: Issues, https://github.com/DaniyalMlk/moneyness/issues
Keywords: options,black-scholes,greeks,implied-volatility,volatility-surface,svi,american-options,quantitative-finance
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mpmath>=1.3; extra == "dev"
Dynamic: license-file

# moneyness

Options pricing, Greeks and implied volatility, in Python with no runtime
dependencies.

The library prices European options under the generalised Black-Scholes-Merton
model, differentiates them analytically, and recovers the volatility implied by
a quoted price. It is written to be correct at the boundaries — zero time, zero
volatility, deep wings, quotes sitting exactly on the arbitrage bound — because
those are the inputs that break naive implementations, and they arrive in real
data more often than the textbook middle does.

## What is checked, and against what

`tests/reference.py` evaluates the same mathematics at fifty decimal digits in
`mpmath`. It shares no code, no rearrangement and no branch structure with the
library: where the implementation has a tail-safe grouping and a limit case,
the reference has the textbook expression and enough precision that the
difference does not matter. Agreement between them across a grid is therefore
evidence about the implementation rather than about whoever typed the expected
values.

The grid reaches the places a naive implementation breaks rather than the
textbook middle — a spot at two fifths of the strike and at nearly four times
it, three days and ten years, a negative carry, volatilities from 5% to 80%.
Every figure below is produced by `examples/worked.py`, which runs in CI and
exits non-zero if any of them moves:

| Check | Result |
| --- | --- |
| The price against the same formula at fifty digits in mpmath, 648 points | worst relative error 1e-12, where the price is above 1e-8 of spot |
| The same, absolutely, including the quotes worth almost nothing | worst absolute error 1e-13, on spots of 50 to 150 |
| The normal distribution function against mpmath's erfc, 1143 points | worst absolute error 1e-15, from z = -40 to z = +40 |
| Delta, gamma, vega, theta and rho against derivatives of that price | worst relative error 1e-11, against a numerical derivative |
| Put-call parity across the same grid | worst relative error 1e-13 |
| Volatility recovered from the price it implies, where vega exceeds 1 | worst error 1e-13 over 342 quotes |
| The width of the set that reprices a quote worth 1.4e-18 | every volatility from 0.001 to 0.899 reprices it |

Four of those rows are shaped by what the measurement will actually support,
and the shape is the interesting part.

**The Greeks are checked against a differentiated reference, not a second
closed form.** A second closed form is another chance to make the same algebra
mistake twice. A numerical derivative of the fifty-digit price shares nothing
with the analytic Greek except the price itself, so an error in the Greek
cannot hide behind an error in its derivation.

**The price gets two rows because one figure would be a lie in one direction or
the other.** Relative error is the right measure for a price somebody would
trade on and a meaningless one for a price of 1e-18, where both
implementations are reporting the last bits of a number that is zero for every
practical purpose and the ratio of two such disagreements can be anything at
all. So the relative bound covers the prices above 1e-8 of spot and the
absolute bound covers every point on the grid, including the ones below it.

**Parity earns its place by being independent of everything above it.** It is
algebra on the two payoffs and does not know what a distribution function is,
so a library whose call and put are both wrong in the same way passes the
reference comparison and fails this.

**And the implied volatility is a ladder rather than a figure**, because a
single round-trip error over this grid would describe the worst *question*
rather than the solver. Vega is how much the price moves per unit of
volatility, and the recovery error tracks it almost exactly:

| vega above | quotes | worst error |
| --- | --- | --- |
| 1 | 342 | 1.1e-14 |
| 0.01 | 386 | 1.8e-12 |
| 0.0001 | 408 | 1.2e-10 |
| 1e-08 | 442 | 7.0e-07 |

206 of the 648 quotes on the grid have a vega of 1e-08 or less and are not
questions a solver can answer at all. The worst of them is out by 0.8 in
volatility, which is not a defect: the quote is worth 1.413e-18 on a spot of
50, and *every* volatility from 0.001 to 0.899 reprices it to within 5e-15.
Eleven standard deviations out of the money an option is worth its intrinsic
value to the same double whatever the volatility, so a monotone function is not
a steep one and a solver always returns a number. Reporting the width of that
set is the honest form of the answer, and it is the only form a test can check.

```bash
pip install -e ".[dev]"      # the reference needs mpmath
python examples/worked.py    # all seven, non-zero if a figure moves
```

Behind them: 6060 tests on Python 3.10 through 3.13.

## Installing

```bash
pip install moneyness
```

> **Not on the package index yet.** The `pip install` line above is what it
> will be; until the first release lands, install from source:
>
> ```bash
> pip install "git+https://github.com/DaniyalMlk/moneyness.git"
> ```

Python 3.10 or newer. The library itself imports only the standard library, so
there is nothing else to resolve and nothing to compile.

Working on it instead of with it:

```bash
pip install -e ".[dev]"
```

`pytest`, `mypy`, `ruff` and `mpmath` are used for development and testing.
`mpmath` is not optional for the suite — see the decision below.

## Releasing

The version lives in `pyproject.toml` and is mirrored by `moneyness.__version__`;
a test asserts the two agree, and the release refuses to run if the tag
disagrees with either.

```bash
# after the version bump has landed on main
git tag v0.1.0
git push origin v0.1.0
```

The tag builds the sdist and the wheel, installs each into a clean environment,
runs the entry point out of both, and then publishes. Publishing uses the index's
trusted-publishing flow, so there is no API token in this repository, in the
workflow, or in the repository's secrets. Registering the publisher on the index
is a one-time step done there, not here, and it names this repository, the
`release.yml` workflow and the `pypi` environment.

## Using it

```python
from moneyness import Inputs, OptionType, price, delta, vega

option = Inputs(spot=100.0, strike=95.0, time=0.5, rate=0.04, vol=0.22)
price(option, OptionType.CALL)   # 10.043415628...
delta(option, OptionType.CALL)   #  0.704045742...
vega(option)                     # 24.433893679...
```

Recovering the volatility from a quoted price:

```python
from moneyness.implied import Quote, solve

quote = Quote(spot=100.0, strike=95.0, time=0.5, rate=0.04, price=10.0434156286)
solve(quote, OptionType.CALL).vol   # 0.22
```

The four market conventions are reached by moving the cost of carry rather than
by calling a different function:

```python
Inputs(100.0, 95.0, 0.5, 0.04, 0.22)                          # non-dividend share
Inputs.with_dividend(100.0, 95.0, 0.5, 0.04, 0.22, 0.015)     # share paying a yield
Inputs.on_future(100.0, 95.0, 0.5, 0.04, 0.22)                # option on a future
Inputs(100.0, 95.0, 0.5, 0.04, 0.22, carry=0.04 - 0.01)       # currency
```

A smile generated by dynamics rather than fitted to quotes:

```python
from moneyness import Contract, Heston, smile

model = Heston(v0=0.04, kappa=1.5768, theta=0.04, sigma=0.5751, rho=-0.5711)
contract = Contract(spot=100.0, strike=100.0, time=1.0, rate=0.0, carry=0.0)
[(p.strike, p.implied_vol) for p in smile(model, contract, [80, 100, 125])]
# [(80, 0.228815...), (100, 0.174341...), (125, 0.151918...)]
```

A variance swap's fair strike, replicated out of the same model's prices and
checked against the closed form it has to agree with:

```python
from moneyness import Contract, Heston, OptionType, fair_variance
from moneyness.heston import price

model = Heston(v0=0.04, kappa=1.5, theta=0.04, sigma=0.5, rho=-0.7)

def otm(strike: float) -> float:
    side = OptionType.CALL if strike >= 100.0 else OptionType.PUT
    return price(model, Contract(100.0, strike, 1.0, 0.0, 0.0), side)

result = fair_variance(otm, forward=100.0, time=1.0, discount=1.0, width=5.0)
result.fair_variance                              # 0.039999999999068...
model.expected_integrated_variance(1.0) / 1.0     # 0.04 exactly
```

## The decision that mattered

The tests check the library against `mpmath` evaluating the same mathematics at
fifty decimal digits, not against constants transcribed from a reference book.

The distinction is not pedantic. A transcribed constant tests the transcription:
if a digit is fat-fingered the suite fails for a reason that has nothing to do
with the code, and — worse — if the implementation and the constant are wrong in
the same way, nothing fails at all. The high-precision oracle shares no code, no
algebraic rearrangement and no branch structure with the implementation under
test. When the two agree to 1e-13 across a grid of several hundred inputs, that
is evidence about the implementation.

It also changes what the tests can assert. Because the oracle is available at
any input, the suite checks *relative* accuracy in the far tail, where the true
value is 1e-89 and an absolute tolerance would be satisfied by returning zero.
That caught the specific reason `norm_cdf` is built on `erfc` rather than `erf`:
the `erf` form loses the entire left tail to cancellation below about x = -8.

The same approach sets the tolerances honestly. The round trip
`norm_ppf(norm_cdf(x))` cannot be exact for x above about 4, because the
probability is then within a few floating-point steps of 1 and the information
is destroyed at storage rather than in the algorithm. Rather than loosening the
tolerance until it passes, the suite asserts against the conditioning bound
`ulp(p) / phi(x)` — which still fails if the upper branch regresses.

## From the command line

```bash
moneyness price  --spot 100 --strike 95 --time 0.5 --rate 0.04 --vol 0.22
moneyness greeks --spot 100 --strike 95 --time 0.5 --rate 0.04 --vol 0.22
moneyness iv     --spot 100 --strike 95 --time 0.5 --rate 0.04 --price 10.0434156286
moneyness ladder --spot 100 --time 0.25 --vol 0.3 --rate 0.03 --low 80 --high 120 --steps 5
moneyness american --spot 100 --strike 100 --time 1 --rate 0.06 --vol 0.25 \
                   --dividend 0.06 --put --steps 400 --boundary
```

`iv` reports the method used, the iteration count and the residual alongside the
volatility, and prints the no-arbitrage range when a quote cannot be inverted.

`american` prints the lattice value beside the European value *on the same
lattice* and the European closed form, so the early-exercise premium and the
discretisation error can be read as separate numbers rather than conflated. It
also prints the Bjerksund-Stensland closed-form approximation and its trigger
price, which is a fast independent check on the lattice rather than a second
opinion from the same machinery. `--boundary` tabulates the early-exercise
boundary, and says so plainly when the exercise region is empty.

## A second decision: what the tolerances mean

Several tests assert against a *conditioning bound* rather than a fixed
tolerance, and the distinction is what makes them worth having.

Recovering volatility from the price of a deep in-the-money option is limited by
the quote, not by the solver. A price is a double, so it moves in steps of
`ulp(price)`; dividing that by `dPrice/dVol` gives the finest volatility the
quote can distinguish at all. For a call struck at 20 against a spot of 100 the
entire time value is 1.4e-9 and that ratio is 2.6e-7 relative — so no solver can
do better, because the information is not in the input.

A fixed tolerance has to be either tight enough to fail on those quotes or loose
enough to stop detecting regressions in the well-conditioned majority. Asserting
against the bound itself avoids the choice, and documents the real guarantee.

The same reasoning fixed a genuine defect. The implied-volatility solver
originally stopped when the price residual was small, which is the natural thing
to write and is wrong: where the price is nearly flat in volatility, that
threshold is met while the volatility is still incorrect in its sixth digit.
Converging on the step in volatility instead — the quantity that is well posed —
brought it to the precision the quote supports, and the disagreement with Brent
that exposed the problem is now a standing test.

## A third decision: why the obvious accuracy trick had to be earned

A lattice price carries an `O(1/n)` error in the layer count, so the textbook
move is Richardson extrapolation: price at `n` and `2n`, and take `2P(2n) - P(n)`
to cancel the leading term. Measured on a European call, doing that to a raw
lattice makes the answer about **twice worse**, not four times better.

The premise is what fails. Richardson assumes the error is a smooth `c/n`, and a
lattice error is not: it carries a large component that oscillates with `n`,
because what the lattice gets wrong depends on where the strike falls between
terminal nodes, and that position jumps around as `n` changes. Subtracting one
price from twice another amplifies exactly that component.

The fix is to remove the cause rather than tune around it. The oscillation comes
from the kink in the terminal payoff being sampled at nodes, so the layer before
expiry is replaced with the closed-form European price — the Broadie-Detemple
smoothing — which integrates across the kink exactly. The remaining error is
smooth, and extrapolation then does what it promised:

| lattice | fine lattice | + Richardson | + smoothing | + both |
|---|---|---|---|---|
| Cox-Ross-Rubinstein | 2.9e-03 | 5.5e-03 | 1.4e-03 | 4.5e-05 |
| Jarrow-Rudd | 2.7e-03 | 6.8e-03 | 1.4e-03 | 5.0e-05 |
| trinomial | 1.6e-03 | 2.3e-03 | 7.2e-04 | 4.7e-06 |

Mean absolute error against the closed form over layer counts from 60 to 120.
Smoothing and extrapolation are each worth a factor of two on their own; together
they are worth 30x to 150x. Both remain switchable, and the failure above is
itself a test, so the default cannot quietly stop being the right one.

## A fourth decision: bracket the approximation, do not just measure it

The American price has no closed form, so there is nothing to check an American
implementation against — which usually means settling for "the two methods agree
to three decimals" and hoping that is enough.

There is something better available here. The Bjerksund-Stensland formula is the
exact value of a *flat-boundary* exercise strategy: exercise the moment the
underlying first crosses a fixed level. That strategy is one the holder could
really follow, just not the best one, so its value is a genuine lower bound on
the American price and an upper bound on the European price:

```
European <= Bjerksund-Stensland <= American
```

The suite asserts that inequality on every row of its grid, for both option
types, rather than only asserting closeness. It is the sharper statement: a
tolerance says two numbers are near each other, while the bracket says which side
of the lattice the approximation has to fall on, and an implementation that
drifted above the lattice would fail it while still looking accurate to three
decimals. A companion tolerance stops the bracket being satisfied the lazy way,
by returning the European value and sitting at the bottom of the band.

Puts are not implemented twice. They route through the call by the
McDonald-Schroder transformation, `P(S, K, T, r, b, v) = C(K, S, T, r - b, -b, v)`,
so there is one exercise rule in the codebase rather than two that can drift
apart — and the transformation itself is asserted, not assumed.

## A fifth decision: let the arbitrage conditions do the work

A volatility surface has to satisfy two conditions to describe a possible
market. Each slice must imply a non-negative probability density — no butterfly
arbitrage — and total variance must not fall as maturity grows at fixed
log-moneyness, or one could buy the longer option, sell the shorter, and collect
a certain profit.

It is tempting to treat these as validation: fit the surface, then check it.
They turn out to be more useful than that, because of what Dupire's identity
looks like in these coordinates:

```
sigma_local^2(k, T) = (dw/dT) / g(k, T)
```

where `w` is total variance and `g` is the Durrleman function — the same `g`
whose sign *is* the butterfly condition. The numerator is non-negative exactly
when there is no calendar arbitrage. The denominator is positive exactly when
there is no butterfly arbitrage. So a local volatility exists, as a real
non-negative number, if and only if the surface admits neither.

The two conditions are not hygiene. They are precisely the conditions under
which "what volatility would reproduce these prices" has an answer at all. So
`local_vol` returns the numerator and the denominator alongside the result,
because when the answer is unusable the caller needs to know which one failed:
a negative numerator is slices crossing, which is a data problem, and a
non-positive denominator is a density defect inside one slice, which is a
fitting problem. They have different fixes.

The choice of interpolation follows from the same thinking. Linear in total
variance at fixed log-moneyness makes the calendar condition automatic — a
linear function between two ordered endpoints is monotone, so non-crossing
slices cannot interpolate to crossing ones. The butterfly condition gets no such
argument, since `g` is nonlinear in `w`. A randomised search over tens of
thousands of admissible, non-crossing pairs failed to find an interpolated
violation, which is suggestive and is not a proof, so the surface recomputes `g`
on interpolated slices rather than assuming it, and checks the midpoints of the
gaps by default.

Calibration exploits the shape of SVI rather than throwing five parameters at an
optimiser. Substituting `y = (k - m) / s` makes the parametrisation *linear* in
the three remaining coefficients, so the fit is a three-variable least squares
solved in closed form inside a two-variable search. The constraint that keeps a
slice non-negative everywhere turns out to be a second-order cone, which is
projected onto exactly rather than clamped coordinate by coordinate.

```python
from moneyness import Surface, calibrate

fit = calibrate(log_moneyness, total_variances)
fit.rmse, fit.slice_.wing_slopes

surface = Surface([(0.25, near), (1.0, mid), (2.0, far)])
[entry.free for entry in surface.calendar()]      # no slices crossing
surface.local_vol(k=-0.2, time=0.8).volatility
```

## A sixth decision: keep the simulation exact, and say what the error is

The Monte Carlo does not run an Euler scheme. Geometric Brownian motion has
lognormal increments in closed form, so a path can be drawn with exactly the
right joint law at its monitoring dates, and there is no step-size bias to trade
against sample count.

That separation earns its keep on barrier options. A barrier monitored weekly
genuinely is a different contract from one monitored continuously — not a worse
approximation to it — and if the simulation carried a discretisation error on
top, the two effects would be impossible to tell apart. Here the only
discretisation is the monitoring schedule, which is a term of the contract.

Standard errors are computed on the count of *independent* samples, not paths.
Under antithetic sampling a path and its mirror are not independent, which is
the entire point of drawing them, so the pair is one sample. Dividing by the
path count instead understates the error by about `sqrt(2)` — silently, and in
the direction that flatters the estimate. The suite checks the reported standard
error against the spread actually observed across sixty seeds, which is the
assertion that would catch it.

Three claims here are measured rather than asserted:

| claim | measured |
|---|---|
| convergence at `n^-1/2` | RMS error over 40 seeds falls by 2.02x, 2.13x, 2.34x per 4x sample increase |
| the geometric-average control on an arithmetic Asian | standard error falls by more than 10x |
| the Brownian bridge on a barrier | step-dependence falls from 1.03 to 0.07 across an 80x sweep of monitoring frequency, about 15x |

Knock-ins are priced as the vanilla minus the knock-out on the same paths, so
in-out parity holds exactly rather than to within two simulations' noise. The
barrier prices are validated against a Reiner-Rubinstein formula implemented
separately in the test suite — the bridge is essentially unbiased at ten
monitoring dates, where the naive estimate is off by many standard errors.

```python
from moneyness import Inputs, OptionType, Settings, Barrier, barrier

option = Inputs(100.0, 100.0, 1.0, 0.05, 0.2)
estimate = barrier(option, OptionType.CALL, 90.0, Barrier.DOWN_AND_OUT, 50)
estimate.value, estimate.standard_error, estimate.interval(0.95)
```

## From the command line, on a whole surface

`surface` reads a CSV of `maturity,strike,vol` — a file, or `-` for standard
input — fits an SVI slice per maturity, and reports what the result implies.

```bash
moneyness surface --quotes quotes.csv --spot 100 --rate 0.05 --local-vol
```

```
 maturity  quotes        rmse     max err         a        b      rho        m        s    left   right    worst g
------------------------------------------------------------------------------------------------------------------------
   0.2500       9   4.504e-12   9.550e-12   0.02000  0.15000 -0.35000 -0.00000  0.12000  -0.203   0.097    0.24751
   1.0000       9   1.420e-11   2.529e-11   0.05000  0.22000 -0.30000 -0.00000  0.18000  -0.286   0.154    0.24879
   2.0000       9   1.808e-11   3.465e-11   0.10000  0.30000 -0.25000  0.00000  0.25000  -0.375   0.225    0.25048

calendar (total variance must not fall as maturity grows)
  0.2500 -> 1.0000  clear, closest approach 0.05052
  1.0000 -> 2.0000  clear, closest approach 0.08452

verdict: no arbitrage found
```

The wing slopes are printed next to the fit quality on purpose: a slope near
Lee's bound of 2 is a warning about the tails whatever the residual says.

It exits 1 when the fitted surface admits arbitrage, so it can be used as a
check in a pipeline. That is a finding about the data rather than a failure of
the request, which is why it is an exit status and not an exception.

## A seventh decision: take the maximum of admissible bounds

The Bjerksund-Stensland approximation is the exact value of a *flat-boundary*
exercise strategy — admissible but not optimal — which is what makes
`European <= approximation <= American` hold by construction rather than by
luck. The 2002 refinement lets the boundary step once partway through the
option's life, which needs a bivariate normal distribution function: the joint
event of not having crossed the first level before the step and finishing in
the money is inherently two-dimensional.

It is tempting to argue the two-step value can never be worse than the flat
one, since a flat boundary is the special case of the two levels coinciding.
That argument is about the *optimal* two-step boundary. The levels here come
from a closed-form heuristic optimised for neither, and measured over 240
random markets the raw 2002 formula falls below the 1993 value on nine of them
— and in one case below the European price, which no American value may do,
since holding to expiry is always available.

So `bjerksund_stensland_2002` returns the largest of the European price, the
1993 value and the two-step value. Each is the value of a strategy the holder
could actually follow, so each is a lower bound, and the largest of several
lower bounds is both the sharpest available and still a bound:

```
European <= max(European, 1993, 2002) <= American
```

That is not a patch over a numerical problem. It is the right way to combine
admissible strategies when none dominates everywhere. It binds on about four
per cent of that grid, and the suite asserts it binds rather than sitting
unused — with the specific market that triggers it written into a test.

| | mean absolute error vs. a converged lattice |
|---|---|
| 1993 single boundary | 0.0666 |
| 2002, guarded | 0.0420 |

The bivariate normal underneath is built from Sheppard's identity with the
substitution `t = sin(theta)`, which removes the `1/sqrt(1 - t^2)` singularity
exactly where it would otherwise bite — at correlations near one, which is
where the two-step boundary evaluates it. Worst absolute error against
fifty-digit integration over 810 points, correlations from -0.9999 to 0.9999
and arguments to eight standard deviations: **2.2e-16**.

Its Gauss-Legendre nodes are computed from the Legendre recurrence at import
rather than transcribed. Forty table entries all look equally plausible, and a
single wrong digit gives a rule that is slightly wrong everywhere — accurate
enough to look fine and never exactly right. The tests check the rule by the
property that defines it: exact on every polynomial up to degree `2n - 1`, and
not beyond.

## An eighth decision: keep two pricing routes, and a broken one

Heston's model has no closed-form price, but it has a closed-form
characteristic function, and a European option is an integral against it. The
integral has been written down more than once. This package evaluates two of
them — Lewis's single integral and Heston's original pair of probabilities —
and the tests require them to agree, to 2e-11 across six maturities, nine
strikes and both option types.

That is not belt and braces. It is the only check available that is sensitive
to the thing most likely to be wrong.

The obvious test — that the price collapses to Black-Scholes as the volatility
of variance goes to zero — passes for *every strike* even with the sign of the
log-moneyness reversed in the integrand. At zero volatility of variance the
transform on the pricing line is real, so `exp(i u k)` and `exp(-i u k)`
contribute identically and the error is invisible. It stays invisible at the
money for any parameters, because there `k` is zero. It shows up only off the
forward and only once the variance is genuinely random — where it prices the
mirror image of the smile the model generates, which for a correlation of
-0.57 put a one-year 200-strike call at 0.2478 against a true 0.0013 — a
hundred and thirty times the lognormal price of 0.0018, for an underlying
whose upside tail the model makes *thinner* than lognormal, not fatter.

A symmetric special case cannot validate a sign that only matters under
asymmetry. Two independent derivations can, and did.

| check | what it catches |
|---|---|
| zero volatility of variance equals Black-Scholes, to 1e-12 | the transform's algebra |
| Lewis and Gil-Pelaez agree, to 2e-11 | the pricing formulas, including signs |
| the Lewis integral re-evaluated at thirty digits in mpmath | the quadrature |
| butterflies non-negative, prices monotone in strike | the shape |

Lewis's route is also the faster of the two, by a factor of 1.45 on a
one-year option, because it is one integral rather than two — so it is what
`price` calls, and the second route stays available for callers who want it
and for the suite.

## A ninth decision: never subtract two things that are about to agree

The characteristic function needs `a - d`, where `d = sqrt(a^2 + sigma^2 m)`.
Those two agree to leading order as the volatility of variance goes to zero,
and their difference is the whole answer: computed directly it is noise below
a `sigma` of about 1e-4, and the integrand stops being smooth enough to
integrate at all — the adaptive rule bisects every panel to its depth limit
and never returns.

`(a - d)(a + d) = -sigma^2 m` gives the same quantity as a division, which
cancels nothing, and it takes `sigma^2` out of the numerator where it was
going to be divided straight back out. The transform is then accurate down to
a volatility of variance of exactly zero, which is the case with an
independent answer, and the gap to Black-Scholes behaves the way theory says
it should:

| | gap to Black-Scholes as `sigma` halves |
|---|---|
| correlated shocks, `rho = -0.5` | halves — leading term is the skew, linear in `rho sigma` |
| uncorrelated shocks, `rho = 0` | quarters — the skew term is absent and the gap is quadratic |

Measured: -0.18779 per unit of `sigma` in the first case, -2.9578 per unit of
`sigma^2` in the second, each stable to better than one per cent over a
sixteen-fold range.

The logarithm in the transform gets the same treatment, since its argument
sits within `sigma^2` of one.

## A tenth decision: implement the broken branch on purpose

Heston's 1993 grouping of the characteristic function takes a complex
logarithm whose argument circles the origin as maturity grows. The principal
branch then jumps by `2 pi i`, and the price is wrong by an amount nobody
would call a rounding error. Albrecher and co-authors' observation is that the
algebraically identical grouping reached by the other square root keeps that
argument inside a disc around 1, where it cannot wind.

Both are implemented, selected by `Branch`, and `branch_discrepancy` measures
the gap. The reason to keep the broken one is that the shape of its failure is
worth seeing:

| transform argument `u` | maturity the textbook grouping survives to |
|---|---|
| 1 | 8.65 years |
| 3 | 2.88 years |
| 10 | 1.55 years |

The failure maturity falls as the frequency rises. So the naive form breaks in
the *tail* of the pricing integrand — the part that shapes the wings — while
the low frequencies that carry most of the price are still correct. A price
computed with it does not look broken. It looks slightly off in the wings,
which is exactly how a model that has been fitted to the wrong wings looks.

## An eleventh decision: simulate the variance, do not repair it

The variance in Heston's model follows a square-root process, and an Euler
step can take it negative. Every repair for that biases the price, and the
bias is not small. Measured against the transform on a one-year
at-the-money call worth 6.809, full truncation — the mildest repair — is out
by **0.718 at four steps a year**, halving with each doubling of the grid:
0.287 at eight, 0.106 at sixteen, 0.039 at thirty-two.

Andersen's scheme instead fits a non-negative law to the exact conditional
mean and variance of the process, choosing between a squared normal and a
point mass plus an exponential by the ratio of the second to the square of
the first. It is **inside its own Monte Carlo error at four steps**, and
stays there. Reaching that with an Euler grid costs upwards of sixty-four
times the work.

The failure is sharpest at the origin, and that is where it matters. Started
at exactly zero, a full-truncation Euler step has no diffusion at all — the
volatility of the increment is the square root of a variance that is zero —
so the path has to be lifted off the origin by drift before any randomness
enters, and the simulated conditional variance comes back below
three-quarters of the true one. A model whose Feller condition fails, which
is the usual outcome of fitting one to index quotes, spends time at exactly
that point.

### What the martingale correction is actually worth

The drift constant can be set per step so the simulated forward is exactly
right rather than approximately right, using the proposal's own moment
generating function. It is often presented as the point of the scheme. It is
not, and the numbers are worth stating because the honest version is more
useful than the impressive one.

The uncorrected constant is chosen so the defect nearly cancels when the
variance sits at its long-run level, and what is left there is third order
in the step. On the reference parameters, per step:

| variance | four steps a year | twenty steps a year |
|---|---|---|
| 0.0025 | +2.06 bp | +0.02 bp |
| 0.04 (= `theta`) | -0.23 bp | -0.002 bp |
| 0.25 | -12.75 bp | -0.12 bp |

Two things to read off that. The defect changes sign as the variance crosses
`theta`, so a path that wanders on both sides of it partly cancels its own
error — which is why the correction is worth less than a glance at the worst
cell suggests. And dividing the step by five divides the defect by about a
hundred and twenty-five, not by five.

So it earns its place on coarse grids and in the wings of a simulated
distribution, where the variance is far from `theta` and stays there, and
does nothing measurable in the middle. It is on by default and can be
switched off.

### Two bugs the tests found

Both in the zero-volatility-of-variance limit, which is the case with an
independent answer and therefore the case worth testing.

The proposal divided by a conditional variance that was exactly zero. And
more interestingly, the log price lost its diffusion entirely. The
correlated part of the price's shock is normally carried by the variance's
own increment, through a substitution that divides by the volatility of
variance — which is why the diffusion coefficients hold only the orthogonal
fraction `1 - rho^2`. As that volatility falls, the variance increment
becomes less random and supplies proportionally less, while the coefficients
in front of it grow like its reciprocal and the total stays right. At
exactly zero the increment is not random at all, the substitution does not
exist, and the correlated fraction has to be put back by hand. The limit is
discontinuous in the decomposition though not in the law.

## A twelfth decision: difference the cheap option, not the dear one

SABR is the smile the interest rate and commodity markets actually quote — four
parameters per expiry, turned into volatilities by Hagan's asymptotic expansion.
`moneyness.sabr` has both of its forms, the displaced variant for negative
forwards, and the Bachelier pricer this package previously had no equivalent of.

```
$ moneyness sabr --forward 0.02 --time 10 --alpha 0.004 --beta 0.5 \
      --rho -0.3 --nu 0.8 --low 0.01 --high 0.035 --steps 7 --floor
forward            0.020000
at-the-money sd    0.00259849
backbone exponent  0.5

    strike    lognormal       normal          black      bachelier      density
-------------------------------------------------------------------------------
  0.010000   24.623602%   0.00355128   0.0010752317   0.0011477077      -7.5881
  0.014167   15.054426%   0.00254592   0.0010808367   0.0011032233      -6.6868
  0.018333    6.941488%   0.00132929   0.0009710369   0.0009737722       9.5840
  0.022500    6.184832%   0.00131248   0.0006954914   0.0006973904      11.4515
  0.026667   10.489246%   0.00243027   0.0008097671   0.0008189376      -1.6095
  ...

the density turns negative below 0.017935, which is 0.79 at-the-money standard
deviations below the forward; ten per cent further down it is -7.1184
```

Three of the degenerate limits are closed forms sharing no code with the
expansion, and they are what the rest rests on. No vol-of-vol with a unit
exponent is Black **exactly** — measured gap 0.0, because every correction term
carries a factor of `nu` or `1 - beta`. No vol-of-vol with a zero exponent is
Bachelier exactly. No vol-of-vol with a general exponent is CEV, where the
formula is *approximate* and wrong by the amount its own next term predicts:
1.704e-03 measured against the 1.713e-03 the `(1-beta)²log²(F/K)/24` term
carries.

### The decision in the heading

The density column above is the second derivative of an option price in the
strike, and the first version of it was differenced from **call** prices
throughout. That is catastrophic cancellation below the forward. A call at a 1%
strike against a 2% forward is worth 0.0094; its last bit is 1.7e-18; four of
those divided by a squared step of 1e-06 is 6e-06 of pure noise — and the
density down there is smaller than that.

The result was a confident, written-up, four-parameter-set finding: *"the
density turns negative below 1.0649%, sixteen standard deviations below the
forward, where it is nine orders of magnitude below its peak — so the defect is
real, always there, and harmless."* Every number in that sentence was rounding.

Pricing the **out-of-the-money** option instead costs nothing — put-call parity
is linear in the strike, so the two second differences are the same number in
exact arithmetic and only one of them is in double precision — and the finding
inverts:

- No one-year smile tested has a negative density **at all** down to 1e-04 of
  the forward, at any vol-of-vol up to 1.2 and any correlation.
- The defect needs maturity or vol-of-vol: across five `nu`, four `rho` and four
  maturities, 61 of 80 combinations have one, and all 19 that do not are
  short-dated.
- Where it appears it is not a tail curiosity. At ten years it is negative
  **0.795 standard deviations** below the forward — inside the range anyone
  quotes — reaching **-7.118** against a peak of 749.7, which is stable to four
  digits across three decades of differencing step. At thirty years the boundary
  reaches 0.15 standard deviations.
- And it is on both wings at ten years, not only the low one: -1.61 at a 2.67%
  strike. `density_floor` searches downwards only, which its name says.

So a short-dated SABR smile is a distribution and a long-dated one is not. That
is a more useful statement than "the formula admits arbitrage", and it was only
available after the arithmetic stopped being dominated by its own rounding.

### Two more things worth stating

The `z / x(z)` factor is zero over zero at the money, and its series is
`1 - rho z/2 + (2 - 3rho²)z²/12`. The **minus** is not a detail: a plus leaves
the series wrong by `rho z`, 3e-03 at a `z` of 1e-02, so it never beats the
ratio at any threshold — which reads as a badly tuned threshold rather than as a
sign error. With the sign right they cross at `|z| ≈ 2e-04` where each is wrong
by about 5e-13, and below that the ratio is wrong by 8.3e-08 at 1e-10 and
8.9e-05 at 1e-12, however small `z` gets.

And `beta` is an argument rather than a fitted parameter because a single smile
carries no information about it. Fitting the same five quotes at four exponents
moves `alpha` by a factor of 15.5 and `rho` from -0.263 to -0.389, while the
fitted smile moves by at most **6.1 basis points of volatility**.

## A thirteenth decision: report the truncation, do not pick a width

A variance swap's fair strike is the one number in this package that needs no
model at all. The identity

```
-log(S/F) + S/F - 1 = ∫₀^F (K-S)⁺/K² dK + ∫_F^∞ (S-K)⁺/K² dK
```

holds pointwise, so its expectation turns realised variance into a portfolio of
the options already quoted. `fair_variance` integrates that in log-moneyness,
where both wings are the same integrand.

Being model-free means there are answers known in advance. A flat smile must
give back `sigma²`, and does, to two or three units in the last place. A Heston
smile must give back `E[∫V]/T`, which `Heston.expected_integrated_variance`
already supplies, and does to 1e-8 relative — **once the strikes reach far
enough**, which is the whole of the difficulty.

The rule of thumb is a few standard deviations of log-moneyness, and under a
flat smile it holds: five leaves 4.4e-08 relative. Under Heston at the same
equivalent volatility it does not. Ten standard deviations leaves 5.7e-07 at a
vol-of-vol of 0.3, 5.3e-05 at 0.5 and **1.3e-03** at 0.8; reaching 1e-09 at 0.8
takes thirty, which is strikes from a quarter of a per cent of the forward to
four hundred times it. The `1/K²` weight is exactly the weight that keeps a fat
tail relevant.

So the width is an argument, and `truncation_error` says in closed form what it
costs. Beyond the last strike the portfolio pays the *tangent* to the log
payoff rather than the payoff, because there is nothing further out to buy, so
the error is the expected excess of one over the other — right to 1e-14
relative against the measurement at one standard deviation. At equal
log-distance the put wing is 1.32 times dearer to lose at one standard
deviation and 2.32 times at four, so a desk one strike short should buy the low
one.

The unbounded version was tried first and does not work, for a reason that
belongs to the prices rather than to the quadrature. A transform price has an
absolute floor: asking for 1e-13 returns 1.48e-12 at log-moneyness 2 and
3.02e-12 at 5 — not monotone — and 1.381e-03 at 40, where the true price is
zero to hundreds of digits. Dividing by the strike makes it harmless to the
integral, but a walk looking for a panel that contributes nothing is looking
for a property the integrand does not have.

### The centring term, exactly

A desk sums over listed strikes and splits puts from calls at `K0`, the largest
listed strike at or below the forward. That replicates the log contract centred
on `K0`, and the identity says what it costs:

```
2 (F/K0 - 1 - log(F/K0)) / T
```

The market convention subtracts `(F/K0 - 1)² / T`, the first term of that. It
overstates the true correction by 0.67% at a one per cent gap, 6.6% at ten per
cent and 32% at a half. `Centring` offers both, and either restores
second-order convergence in the strike spacing — halving ratios of 4.00 —
against an uncorrected sum whose ratios run 6.64, 10.71, 2.65, 1.71, 7.23 and
1.46, because its error is set by where the forward falls on the lattice rather
than by the spacing.

### And a volatility swap is not its square root

`integrated_variance_variance` gives `Var(∫V)` in closed form for the
square-root process, from `Cov(V_s, V_t) = e^{-κ(t-s)} Var(V_s)` integrated
twice; it agrees with an independent double quadrature of the same covariance
to 1e-14. `volatility_swap_strike` turns that into the second-order fair
volatility — and the docstring says what the measurement says, which is that
the correction has the right sign and is a quarter to a half too large. Against
60,000 simulated paths the predicted discount is 0.0195 in volatility against
0.0153 realised at a vol-of-vol of 0.5 and 0.0343 against 0.0245 at 0.8: 13 and
22 standard errors, so not sampling noise.

```bash
moneyness variance --forward 102.37 --time 1 --rate 0.03 --vol 0.2 \
  --width 2 --step 5
```

prints the continuous replication with its exact answer and predicted
truncation, then the listed-strip sum at all three centrings side by side.

## A fourteenth decision: three derivations for one price

A barrier option is the first contract here that depends on where the path went
rather than only on where it ended, so the reflection principle has to come in.
`moneyness.barrier` gives the eight standard single-barrier Europeans in closed
form, and the table they come from is self-consistent: `in + out = vanilla`
holds algebraically for all eight, so asserting it to 7.1e-15 catches a term
copied with the wrong sign and nothing more. It cannot catch the table being
the wrong table.

So the same contract is priced two other ways, and neither shares a derivation
with the formula. `price_pde` now takes a knock-out and marches a grid
backwards with a Dirichlet condition at the barrier; across seven contracts it
converges on the closed forms at ratios of **4.00** under doubling of both mesh
dimensions, which is the order the scheme supports, so the two agree at the
mesh's own accuracy rather than to a chosen tolerance. And `monte_carlo.barrier`
with its Brownian bridge reaches them from paths, within **0.8 standard errors**
across all four styles. A formula, a grid and a simulation.

```bash
moneyness barrier --spot 100 --strike 100 --time 1 --rate 0.05   --dividend 0.03 --vol 0.20 --level 120 --barrier up-and-out
```

### The barrier outranks the strike on the grid, and it is measured

`_space_nodes` has always anchored the lattice on the strike, because the
payoff kink must sit on a node, and deliberately not on the spot — demanding
both collapsed the spacing, and a strike 1% from the spot once produced 1781
nodes where 600 were asked for. A barrier is a third special level and it wins:
the domain ends *at* the barrier, so a barrier between nodes is a barrier
moved by up to half a spacing, which is first order in a quantity the price is
very sensitive to.

Measured on an up-and-out call, displacing the barrier by half a spacing moves
the price by **35, 69, 137 and 273 times the on-node error** as the mesh refines
from 200-by-100 to 1600-by-800. The ratio *grows*, which is the signature of the
lower-order term taking over. Both levels keep their nodes where they are more
than a step apart, by choosing the number of steps between them rather than the
spacing; closer than that the node count is capped at twice the request and the
strike is given up, with `MeshPrice.strike_on_node` saying which happened. That
cap was eight times the request first, and let a barrier five basis points from
the strike build 2442 nodes for a request of 400 — the same collapse the strike
anchoring exists to avoid, under a guard meant to prevent it.

The bug worth recording was in the domain, not the spacing. Its reach was sized
from the barrier rather than from the spot, so a barrier at ten times the spot
put the whole grid above the spot: the quadratic read extrapolated off the
nearest three nodes and returned **-1183.8 for a call worth 8.65**. A wrong
answer with a plausible shape is the worst outcome this code can produce. The
only test that would have caught it was the one comparing a distant barrier
against the unbarriered solve — every comparison against the closed form uses a
barrier within 20% of the spot, where the mis-sized domain still happened to
contain it. The containment is now swept directly over both sides, barriers from
1% away to ten times the spot, four widths and four node counts.

### A knock-out is the one price here that falls as volatility rises

Everything else in this package is increasing in volatility. An up-and-out call
peaks at a volatility of **7.51%** and falls away on both sides: on a spot of
100, a strike of 100, a barrier of 120 and a year to run it is worth 3.435 at
the peak, then 3.117, 1.923, 1.107, 0.662, 0.418 and 0.191 at 10% through 40%.
From the peak to 40% it loses 94% of its value for *more* volatility, because
the volatility that pays for the optionality also pays for the knock-out. Vega
is +31.0 at 5% and -11.9 at 20%, so it changes sign inside the range anybody
quotes, and an implied-volatility solve has two roots or none at almost every
price. None is offered, and the command line says so where someone would look
for one.

### The continuity correction's accuracy is simulated, not assumed

A barrier checked at the close is harder to breach than one watched
continuously, so a knock-out is worth more. `monitoring_shift` applies the
Broadie-Glasserman-Kou correction — move the level away from the spot by
`exp(0.5826 sigma sqrt(dt))` and price the continuous contract there — and the
shift is under a per cent while what it is worth is not: the price is levered
about eighteen times to the barrier level.

`monte_carlo.barrier` with `bridge=False` prices the genuinely discrete
contract, which is a different contract and not a worse estimate of the
continuous one, so it says what the correction is actually worth. The real
effect is **+13.3%, +28.3% and +54.7%** over continuous monitoring for daily,
weekly and monthly checks. And the correction itself is **-0.33%** off daily,
**+0.92%** weekly and **+6.82%** monthly — excellent daily, fine weekly, and at
monthly monitoring it overstates the contract by seven per cent. Still far
better than ignoring the effect, which understates the monthly contract by
thirty-five, but not a number to quote. Simulate that one, and the docstring
and the command line both say so.

## A fifteenth decision: bound it, because it cannot be priced

Every price above is a formula, a grid or a simulation of something whose
distribution is known. The arithmetic average of lognormals has no tractable
distribution, which is why `monte_carlo.asian` simulated it and nothing computed
it. The gap that leaves is not speed. **A simulation cannot bound anything** — it
returns an estimate and a standard error, so it can say a price is probably near
a number and never that it is certainly above or below one.

`moneyness.asian` gives two bounds that are inequalities rather than
approximations, each resting on a statement about the payoff that holds path by
path. AM-GM: `G <= A`, so the geometric call is cheaper. Convexity: the positive
part of an average is at most the average of the positive parts, so an Asian is
worth at most the average of ordinary options on its monitoring dates. Between
them sits Curran's bound, which conditions on the geometric average instead of
discarding it.

```python
from moneyness import Inputs, OptionType, price_bounds, turnbull_wakeman

contract = Inputs(spot=100.0, strike=100.0, time=1.0, rate=0.05, vol=0.20)
bounds = price_bounds(contract, OptionType.CALL, 12)

bounds.lower       # 6.1556 -- Curran's, within 6e-05 relative of the true price
bounds.upper       # 6.8370
bounds.geometric   # 5.9402 -- below, by AM-GM, for a call
turnbull_wakeman(contract, OptionType.CALL, 12)   # 6.1742, and high
```

### Which side AM-GM falls on depends on the payoff

The inequality that makes the geometric **call** cheaper makes the geometric
**put** dearer, since `max(K - G, 0) >= max(K - A, 0)`. At the money with 20%
volatility and twelve fixings: a geometric call of 5.9402 under a true 6.1571, and
a geometric put of 3.6517 *over* a true 3.5355. Treating it as a lower bound for
both sides would have looked right on every call that was tried.

For a put it is therefore an upper bound, and a much better one than convexity —
which allows the fixings to be independent, where averaging destroys far more
variance than that. The put's interval comes out **0.118** wide against the call's
**0.681**, so it is the one used.

### Conditioning is worth a factor of six hundred

Curran's bound exercises when `E[A | G] > K`. That is a strategy a holder could
follow, so its value is below the option's, and because the two averages move
together it is nearly exact. Against two million paths it is low by 6.0e-05
relative at 20% volatility, 3.2e-04 at 40% and 1.5e-03 at 80% — against the
geometric bound's 0.2154 at the money, a factor of about six hundred. At 10%
volatility the shortfall sits at 0.3 standard errors of that run, so it is
reported as unresolved rather than as a number.

### The moments are exact, and they say why an Asian is cheap

`E[S_i S_j] = S^2 exp(b(t_i + t_j) + v^2 min(t_i, t_j))` makes both moments finite
sums. Twelve monthly fixings leave the average with a matched log-variance of
**0.01526** against the terminal price's 0.04 at 20% volatility — an effective
volatility of **12.35%**. The dense-fixing limit is the textbook `v^2 T / 3`,
approached as `1/3 + 1/(2m)`: at three thousand fixings that predicts 0.333500
against a measured **0.333503**.

That third is a *zero-carry* statement. At a 5% cost of carry the limit sits at
0.3377, because `exp(b t)` weights the later and more variable part of the path
more heavily. The test asserted a third with a carry in force and failed by eight
times its tolerance.

### Moment matching's error changes sign twice

At the money it reads high — 6.1e-04, 1.4e-03, 3.0e-03, 8.5e-03 and 3.0e-02
relative at 5%, 10%, 20%, 40% and 80% volatility over a year, two to three times
worse per doubling. Out of the money it reads **low enough to break the lower
bound**: a call struck at 120 against a spot of 100 comes out 9.2% below Curran's
bound at 10% volatility, 3.4% below at 20% and 0.79% below at 40%.

Then it crosses: at 60% the same option is 0.9% *above* the bound and at 80% it is
2.7% above. So the shrinking violations at moderate volatility point the wrong way
about what happens next. The 40% figure was first written down as 7.9%, from
misreading `-7.86e-03` as a percentage, which made the trend look monotone and hid
the crossing.

An error that is positive at one strike and negative at another, and that reverses
again in volatility, cannot be quoted as a tolerance — so none is. The bounds say
when it has gone wrong instead, which nothing else here could: there is no closed
form to compare against, and at low volatility the simulation's standard error is
wider than the error being looked for.

```bash
moneyness asian --spot 100 --strike 120 --time 1 --rate 0.05 --vol 0.20 \
  --fixings 12 --fixing-table
```

## A sixteenth decision: let a bound with no correlation in it be the check

An option on `S1 - S2` has no closed form, so the module carries three routes:
Margrabe's exchange option, which is exact at a zero strike; Kirk's
approximation, which is what desks quote; and an exact conditional quadrature.
Conditioning is the one that does the work. Given the variate driving the second
asset, the first is still lognormal with a shifted forward and a reduced
variance, so the conditional payoff is a Black-76 call struck at `S2(z) + K` and
the price is one integral against a normal density.

```bash
moneyness spread --forward1 100 --forward2 95 --strike 1 \
    --time 1 --rate 0.03 --vol1 0.30 --vol2 0.25 --rho 0.9
```

```
              contract  call on S1 - S2 struck at 1
        forward spread  5.0000000000
            spread vol  0.1322875656

                 price  7.2080907571
           resolves to  2.155e-10  (absolute)

    lower (half-space)  7.2080631333
      upper (vanillas)  7.7857973177
        interval width  0.5777341844
                        0.0802 of the price

        exchange (K=0)  7.7857973177  (exact, Margrabe)
                  Kirk  7.2079860954
                        -1.452e-05 relative
                        outside the bounds, below the lower bound by 7.704e-05
```

The last line is the decision in the heading. Kirk's error here is 1.5e-05
relative, which anybody would accept — and the number is nonetheless below a
price that can be sub-replicated, so it is not a slightly-off price but an
arbitrage. Nothing except a rigorous bound can say that, and the same bound says
it at 18 of the 84 strike-correlation points swept: fifteen below the lower
bound, and three *above* the upper one by up to 1.9e-02 at a correlation of
-0.99.

### The upper bound is a correlation reading

For any level `a`, `max(S1 - S2 - K, 0) <= max(S1 - a, 0) + max(a - S2 - K, 0)`
path by path — the split either hands the whole shortfall to one leg or divides
it exactly. So two vanilla options bound the spread, with no correlation in them
at all.

That is also the limit of what they can do. A bound that holds for every
coupling of the two marginals is the price under the worst one, and for
lognormals the worst coupling is attainable. So the optimised portfolio does not
approximate the upper extreme; it **equals** the spread price at a correlation of
minus one, measured at 4.2e-14 to 6.7e-14 relative across five strikes. Two
routes with nothing in common — two Black formulas against a conditional
quadrature — agreeing to machine precision is worth more than either agreeing
with a tolerance.

Which explains the width above. The bracket is 1.1% of the price near a
correlation of minus one and 484% wide at plus 0.9, because the width measures
distance from the worst case rather than the quality of the method. At a zero
strike it closes entirely: `S1 > S2` is already a linear condition on the
driving Gaussian, so the best sub-replicating half-space *is* the exercise
region and the dominating exchange option is the same price.

### The kink is worth eight orders of magnitude, and missing it looks like divergence

With no volatility on the first asset the integrand is genuinely kinked at the
point where the conditional option passes through the money. Uniform panels at
24, 48, 96 and 192 panels give relative errors of 9.6e-06, **1.8e-05**, 3.3e-06
and 1.2e-06 against the closed form. The first refinement makes it *worse*, so a
two-point convergence check would have reported the method diverging rather than
crawling. With the root of `F1(z) - S2(z) - K` inserted as a panel edge, the same
quadrature is exact to 4.5e-14.

### Two monotonicity claims that measurement contradicted

Kirk's error grows with the strike — at every one of the six non-positive
correlations swept, where it reads high throughout. At correlations of 0.5 and
0.95 the sign change lands inside the strike range and the ordering fails. The
first version of that test was tried on negative correlations and passed on a
claim that is false.

And the half-space lower bound's gap is not monotone in the correlation. It is
exact at both ends, since a single driving variate makes the exercise region a
genuine half-space, and peaks in between: 3.1e-10, 1.0e-06, 5.9e-06, 2.4e-05,
6.9e-05, 1.2e-04, then back to 1.0e-06 as the correlation runs -0.99, -0.9, 0,
0.5, 0.8, 0.95, 0.999. Any sweep stopping at 0.9 would have shown it growing
throughout.

## A seventeenth decision: one tail integral, not four closed forms

A lookback pays on where the path got to. There are four of them — the extreme
against a strike, and the terminal price against the extreme, each way round —
and the usual treatment is four closed forms copied out of a table. They are all
the same integral. Write the payoff as a layer cake,

```
(M_T - L)^+ = int_L^inf 1{M_T > y} dy
```

and the expectation is an integral of the running maximum's tail, which the
reflection principle gives directly. So `moneyness.lookback` has two functions
that matter, `maximum_excess` and `minimum_shortfall`, and the four contracts,
the expected extremes and the relations between them are rearrangements of
those.

```bash
moneyness lookback --spot 100 --strike 100 --time 1 \
    --rate 0.05 --carry 0.05 --vol 0.2 --monitorings 252 52 12
```

```
              contract  fixed-strike call, extreme so far the spot
                 price  19.1676252573
               vanilla  10.4505835722
               forward  105.1271096376
            E[maximum]  120.1503704192
            E[minimum]  87.0275830734
        P(max <= spot)  0.0
        P(min <= spot)  1.0

  per year        price      std err   vs continuous
       252    18.372059     0.033563           -4.15%
        52    17.416833     0.033243           -9.13%
        12    15.812022     0.032355          -17.51%
```

Two of those probabilities are exact rather than tight. A Brownian path leaves
its starting point in both directions immediately, so the maximum exceeds the
spot and the minimum falls below it with probability one — and the reflection
formula evaluated at that point is a difference of two equal terms, which
returns something near 1e-17 rather than nothing. Harmless on its own, and not
harmless inside a tail integral.

### The textbook form divides by zero on an option on a future

Integrating the tail analytically leaves a factor of `sigma^2 / (2b)` against a
difference of two terms that become equal as the carry vanishes. The direct
transcription therefore divides by zero when `b` is exactly zero — which is
what `Inputs.on_future` sets it to, so the contract that breaks it is one of the
conventions this package advertises in its first paragraph.

Pulling the shared exponential out instead leaves an `expm1` and a band of the
normal law between two symmetric arguments, both of which survive the limit. The
two groupings then fail in opposite regimes, and that is the finding: the direct
one is accurate to 2.0e-15 at a 1% carry and degrades to 2.4e-12 at 1e-6 and to
5.7e-10 at a level 2.7 times the spot, while the regrouped one holds those same
cases to 1e-14 or better and is wrong by a *factor of 4e+24* once `2b / sigma^2`
reaches 100, because the exponential it factors out then dwarfs the difference
it multiplies.

Over 7,840 combinations of level, carry, volatility and maturity — 6,765 of them
above a floor of 1e-8 times the spot, below which a relative error measures
round-off rather than method — switching at `|2b / sigma^2| = 1` gives a worst
relative error of 3.3e-13 against 8.4e-09 for the direct form alone. The
crossover is not tuned: the same sweep gives 3.3e-13 at 0.5 and at 2.

### Monitoring is worth more than the correction that would represent it

`moneyness.barrier` can tabulate discrete monitoring in closed form, because a
barrier checked at the close is representable as a barrier at a shifted level.
A lookback cannot: the correction is to the extreme itself and there is no level
to move. So the monitoring table above is simulated, and the simulation is of
the discrete contract rather than of a correction to the continuous one.

The gaps are large and they point the other way from a barrier's. A maximum
taken at the close is smaller, so every lookback is worth less: 4.2% less daily,
9.2% weekly and 17.8% monthly for the fixed-strike call struck at the spot, and
3.4%, 7.5% and 14.7% for the floating-strike call. Quoting the continuous price
for a monthly fix is an 18% error.

That simulation is also the only check here that owes nothing to the reflection
principle, so it has to be unbiased, and a path observed on a grid is not. Its
maximum is smaller than the maximum of the path it was drawn from, and the bias
dies like one over the square root of the step count rather than fast enough to
spend steps on. Conditional on its two endpoints a Brownian path over a step is
a bridge, whose maximum has an invertible distribution function, so one extra
uniform per step draws the true extreme. Over five seeds at 40 steps the
simulated prices land between -1.2 and +1.7 standard errors of the closed form
and scatter either side; the same run with the bridge off is 101 standard errors
low.

### A property that was true and the wrong property

`Inputs.is_degenerate` asks whether the terminal payoff carries any
uncertainty, and a zero strike is one of its cases: a call struck at nothing is
worth the forward whatever the path does. A lookback's strike says nothing about
the path — a floating-strike one does not read the strike at all — so reusing
that property priced a zero-strike floating-strike call off a deterministic path
and returned **4.98** where the answer is 17.56. What matters here is whether
the *path* is deterministic, which is no time or no volatility and nothing else.
A test asserting that the floating styles ignore the strike is what caught it,
and that test exists because the docstring claims they do.

## An eighteenth decision: let the decomposition be the implementation's judge

Every payoff above is fixed when the trade is struck. Two are not: at a date
before maturity the holder decides something, and what they hold afterwards
depends on the decision. A **chooser** defers the direction. A **compound
option** defers the purchase.

```python
from moneyness import Compound, Inputs, OptionType, chooser_legs, chooser_price, compound_price

inputs = Inputs(100.0, 100.0, 1.0, 0.05, 0.25, carry=0.05)

chooser_price(inputs, 0.5)            # 17.041176  -- declare call or put at six months
chooser_legs(inputs, 0.5).total       # 17.041176  -- the same, from two vanilla prices

bought = compound_price(inputs, Compound(OptionType.CALL, OptionType.CALL, 5.0, 0.5))
bought.value                          #  8.381446
bought.forward_purchase               #  7.459449  -- buying it forward instead
bought.deferral_value                 #  0.921997  -- the right to decline
bought.critical_spot                  # 93.720886  -- where the inner call is worth 5.00
```

### The chooser's decomposition is the contract, not a check bolted on

At the decision date the holder has `max(C, P)`, which is `C + max(P - C, 0)`,
and put-call parity makes `P - C` a known linear function of the spot. So the
choice is worth a call struck at `K` to maturity **plus** `exp((b - r)(T - t))`
puts struck at `K exp(-b (T - t))` expiring at the *decision* date — two
`bsm.price` calls. Rubinstein's closed form is the same thing written as four
normal integrals, and the two agree to **1.8e-14** at worst over strikes from 80
to 120, carries from -0.03 to 0.08 and volatilities from 15% to 60%.

Both are kept, because a closed form with an independent derivation to check it
against is worth more than either alone. The decomposition also hands over the
two limits: at a decision date equal to the maturity the chooser is a straddle,
and at a decision date of zero it is the larger of the two vanillas — reached
exactly, since the closed form's `y` divides by the square root of zero there
and the branch has to be right rather than nearly right.

### One parity validates both halves of a compound pair at once

A call-on-call pays `max(C - K1, 0)` at the decision date and a put-on-call pays
`max(K1 - C, 0)`, so their difference is `C - K1` pathwise and the discounted
difference is the inner option less the discounted premium. Nothing from this
module is on the right-hand side. The residual is at worst **7.1e-15** across
four parameter sets per pair, and it constrains four bivariate arguments and two
correlation signs for each pair at once — which is what makes the put-side
formulas trustworthy, since an error in either member breaks it.

An antithetic simulation agrees independently: only the spot at the decision
date is drawn and the inner option is priced in closed form there, so the
estimator has no discretisation error at all. Across five seeds for each of the
four pairs the twenty deviations run from -1.52 to +0.93 standard errors and
take both signs within every pair, which one seed inside its interval could not
have shown.

### The payoff algebra does not reduce to one option in every case

With the decision date at the maturity the inner option is worth its intrinsic,
and three of the four compounds become a single vanilla on a shifted strike. The
fourth does not. Capping `max(K1 - (S - K)^+, 0)` truncates the payoff at both
ends — flat at the premium below the inner strike, zero above `K + K1` — so it
is a put struck at `K + K1` **less** a put struck at `K`. Writing it as the
single shifted put overpays by the whole flat region: **9.881 against a correct
2.422**, four times the price, with the overpayment concentrated exactly where
the contract is most likely to pay.

### A compound option is a forward purchase plus an option on the premium

```console
$ moneyness deferred --spot 100 --strike 100 --time 1 --rate 0.05 --vol 0.25 --premium 5
...
  compound options on a premium of 5 paid at the decision
            contract          price    forward buy      decline  P(exercise)
        call-on-call       8.381446       7.459449     0.921997     0.662712
         put-on-call       0.921997       7.459449            -     0.337288
    on a call: parity residual -2.665e-15, critical spot 93.720886
```

The right to decline, 0.921997, is the put-on-call to the last digit printed,
which is what the parity residual beside it is asserting. It is also why a
compound option is not a cheap way to buy an option: it costs more than buying
the thing forward, by exactly the value of the choice.

## A nineteenth decision: let the correlation disagree with itself

An option on a share quoted abroad has four common forms, and they are not
variants of one price. Writing `X` for the exchange rate in domestic units per
one foreign unit:

| contract | payoff | correlation |
| --- | --- | --- |
| foreign struck | `X_T (S_T - K_f)+` | absent |
| quanto | `X_fix (S_T - K_f)+` | lowers a call |
| composite | `(S_T X_T - K_d)+` | raises a call |
| equity-linked FX | `S_T (X_T - K_x)+` | raises a call |

```bash
moneyness quanto --spot 100 --exchange 1.25 --strike 105 --time 1 \
  --domestic-rate 0.04 --foreign-rate 0.02 --dividend 0.01 \
  --asset-vol 0.22 --exchange-vol 0.14 --rho 0.35
```

```
          foreign strike  105.0000000000
         domestic strike  133.9014258785
             rate strike  1.2752516750
            quanto drift  -0.0107800000
           composite vol  0.2992657682

          foreign struck    8.7775022095   d/drho    +0.000000
                  quanto    8.0004023524   d/drho    -1.677126
               composite   12.6889317588   d/drho    +5.071480
        equity linked fx    7.5531410400   d/drho    +1.897304
```

One strike cannot serve four contracts, so the command converts the foreign
strike at the forward rate — the conversion under which the three struck
contracts become one contract when the exchange rate stops moving. The slopes
are why they are printed together.

### The correlation enters with a minus in one place and a plus in another

`quanto_drift` is `-rho sigma_S sigma_X` and `composite_volatility` is
`sqrt(sigma_S^2 + sigma_X^2 + 2 rho sigma_S sigma_X)`, so one estimate moves two
of these prices in opposite directions. Over correlations from -0.8 to +0.8 the
quanto loses 28.1% of its value, the composite gains 162.5% and the
equity-linked option gains 56.1% — and the foreign-struck price does not change
by a single bit, because converting a foreign payoff at whatever rate prevails
is holding the foreign claim. That invariance is asserted rather than assumed;
it is the one contract here where a correlation estimate cannot be wrong.

### Which way up the rate is quoted decides the sign

The other quotation negates the correlation and leaves its magnitude alone, so a
figure taken from the wrong convention is exactly as plausible and exactly
wrong. The quanto drift then moves by `2 rho sigma_S sigma_X` instead of
vanishing: a quanto call reads 8.6% too high at a correlation of 0.2, 15.5% at
0.35 and 41.9% at 0.85, with nothing in the price to say so.
`CrossCurrency.inverted` changes quotation properly.

### Neither quote identifies the pair; the two of them do

A quanto price depends on `(sigma_X, rho)` only through the product
`rho sigma_X`. Five pairs along that curve, `sigma_X` from 0.05 to 0.50, give
quanto prices equal in every bit while their composite prices differ by 42%. A
composite price depends only on `sigma_X^2 + 2 rho sigma_S sigma_X`; three pairs
along that curve agree to 1e-13 while their quanto prices differ by 19%.

Two different functions of the same pair, and together linear in
`(rho sigma_X, sigma_X^2)`, so `implied_fx_pair` inverts them with no search.
The closed form is also what makes the failure legible: inconsistent quotes
produce a negative variance and are refused, where a two-dimensional search
would have stopped at a plausible corner with a poor residual.

### A degenerate case is the only check on the strike conventions

At zero exchange rate volatility, with the foreign strike converted at the
forward rate and the quanto's fixed rate set to that forward, all three struck
contracts equal today's rate times the foreign-currency option to within six
parts in 1e15. A simulation cannot catch a strike written in the wrong currency,
because the simulation would be wrong the same way; this collapse can.

## Running the tests

```bash
pytest                 # test suite
mypy                   # strict type checking, sources and tests
ruff check .           # lint
```

The same three run on every push and pull request, and the suite runs on each
Python version the package claims to support rather than only the newest. That
matters more here than it would in most projects: there are no dependencies to
resolve differently, so an interpreter difference surfaces directly as a
difference in floating-point or `math` behaviour — which is precisely what a
single-version matrix is the wrong shape to catch.

A fourth job builds the wheel, installs it into an empty virtualenv with no
source tree in sight, and runs the console entry point. An entry point can be
perfectly healthy in a source checkout and broken the moment it is installed,
and nothing short of installing it will say so.

## Status

Phases 1 to 3 of [the roadmap](ROADMAP.md) are in place — the pricing core, the
Greeks, and implied-volatility solving — along with phase 4, which adds American
exercise on binomial and trinomial lattices with the early-exercise boundary and
the Bjerksund-Stensland closed form, and phase 5, the volatility surface: SVI
slices and their calibration, both arbitrage conditions, and Dupire local
volatility. Most of the command-line interface from phase 7 is there too.

Phase 6 is in place too: an exact Monte Carlo with antithetic and control
variates, Asian and barrier payoffs, and the Brownian bridge correction.

Phase 7 is complete: the command line covers pricing, Greeks, implied
volatility, a strike ladder, a term structure, American valuation and surface
fitting with arbitrage reporting.

Phase 9 adds a second model: Heston's stochastic volatility, its
characteristic function in the branch-stable grouping, two independent
pricing routes, and the quadrature they run on — all still in the standard
library, so the package remains dependency-free.

Phase 10 simulates the model rather than transforming it: Andersen's
quadratic-exponential scheme, Asian and barrier payoffs under stochastic
volatility, and an independent check on the transform that shares no code
with it.

Phase 11 adds SABR: both of Hagan's expansions, the displaced variant, a
Bachelier price the package lacked, calibration, and a search for where the
formula's own density turns negative.

Phase 12 adds variance and volatility swaps: the log-contract replication with
its two exact targets, the closed-form cost of a finite strike range, the
listed-strip sum with the centring term computed exactly rather than to leading
order, and the variance of integrated variance that separates a volatility swap
from the square root of a variance one.

Phase 13 closes the gap the surface left: the package computed a Dupire local
volatility and could not price with it. There is now a finite-difference
solver in log-spot for a general local volatility, European and American, with
the early-exercise boundary; a bridge from the surface that does the
forward-log-moneyness mapping in one place; and a `--reprice` flag on the
`surface` command that solves every quote back out of the surface fitted to it.
The round trip is the point: a surface's own local volatility reproduces the
surface to within 1.5e-03 in volatility, and the two ways of getting it wrong —
reading the moneyness from the spot, and taking the surface literally below its
first quote — are both measured rather than warned about.

Phase 14 adds barrier options, the first contract here that depends on where the
path went: the eight closed forms, a knock-out in the solver, and the existing
simulation, all three agreeing.

Phase 15 is the first price in the package that cannot be computed at all, only
bracketed. The arithmetic average of lognormals has no tractable distribution, so
`moneyness.asian` supplies the exact moments, Curran's conditioning lower bound,
and rigorous bounds from AM-GM and convexity — which is what lets it show that
the moment-matched price every trading system uses falls *below* a rigorous lower
bound out of the money, by 3.4% at 20% volatility.

Phase 16 adds the first contract on *two* underlyings. `moneyness.spread`
prices an option on `S1 - S2` by conditioning — exact up to the quadrature,
pinned against Margrabe at a zero strike — alongside Kirk's approximation and a
rigorous interval. The upper side of that interval is built from two vanilla
options and carries no correlation at all, which turns out to equal the price
at a correlation of minus one to 1e-13; and inside it, Kirk sits outside the
bounds at 18 of 84 points swept.

Phase 17 adds the other half of what the reflection principle gives. Where
phase 14 asked whether the running extreme crossed a level, `moneyness.lookback`
prices contracts on where it got to — all four from one analytic integral of the
extreme's tail, with the law of that extreme public because a stop-loss or a
high-water-mark fee is written on it and neither is an option. The carry appears
as a `sigma^2 / (2b)` that the textbook form divides by, so an option on a
future breaks it; two groupings that fail in opposite regimes hold it to 3.3e-13
across the sweep. And the monitoring table has to be simulated rather than
corrected, which is where the 17.8% gap at a monthly fix comes from.

Every item on [the roadmap](ROADMAP.md) is now done except the first release
on the package index, which waits on the publisher being registered there.
