Numerical Correctness and Testing
How quants prove their code is right: analytic checks, boundary cases, property-based invariants, and tolerances that respect floating point.
Leads to: Every backtest and pricer in Phases 13–18 must ship with the tests this lesson teaches.
Learning Objectives
Click a status chip to cycle: Not started → In progress → Studied → Practiced → Needs review → Mastered.
- Explain why floating-point equality must be replaced by tolerance-based comparison.
- Implement a unit test that catches a boundary bug in a pricing function.
- Design a property-based test from a mathematical invariant (e.g. put–call parity).
- Compute an appropriate absolute/relative tolerance for a numerical assertion.
- Explain the difference between a bug that raises and a bug that returns a plausible wrong number.
Key Vocabulary
- Unit test
- A small automated check that a function returns the expected output on specified inputs, run on every change.
- Oracle
- A trusted reference answer - a closed-form value, a slow-but-correct implementation, or a mathematical identity.
- Boundary case
- An input at the edge of validity (zero volatility, expiry \(T=0\), empty array) where naive code often breaks.
- Property-based test
- A test asserting an invariant that must hold for many random inputs (monotonicity, parity, symmetry), not one fixed case.
- Tolerance
- The allowed gap
atol/rtolin a floating comparison, chosen from the algorithm’s error, not arbitrarily.
- Regression test
- A test that pins a previously fixed bug so it can never silently return.
Intuition & Motivation
Because floating-point arithmetic is inexact, you almost never assert a == b on computed reals. You assert |a-b| ≤ \text{atol} + \text{rtol}\,|b|. And because you cannot enumerate all inputs, you test three things: known answers (oracles), the edges (boundaries), and properties that must hold for every input.
Never compare floats with ==
Real arithmetic in IEEE-754 rounds every operation. So \(0.1+0.2 \ne 0.3\) exactly. The correct assertion uses a tolerance, matching NumPy’s rule:
Pick rtol from the algorithm’s accuracy (a Monte Carlo estimate with \(10^4\) paths has standard error \(\sim 1/\sqrt{10^4}=10^{-2}\), so a 1e-9 tolerance is nonsense). Pick atol for quantities near zero.
Three kinds of test, in order of value
- Oracle tests. Compare against a value you trust: a closed form (Black–Scholes at a known point), or a slow reference (the loop from 17.1).
- Boundary tests. Feed the edges: \(T=0\), \(\sigma=0\), a deep in/out-of-the-money strike, an empty or length-1 array. These expose
0/0,log(0), and off-by-one errors. - Property tests. Assert invariants over random inputs: a call price is non-negative, increases in \(S\), and satisfies put–call parity \(C-P=S-Ke^{-rT}\).
nan at \(T=0\); a downstream sum then poisons the whole portfolio value with nan.assert bs_call(S=100,K=90,r=0.01,sigma=0.2,T=0) == max(100-90,0). It fails on the naive code, passes once you special-case \(T=0\).abs((C-P) - (S-K*exp(-r*T))) < 1e-9 (put–call parity).Why parity is a great test
Put–call parity is model-free: it follows from no-arbitrage, not from Black–Scholes. So it must hold for any correct European call/put pricer, at every input. A single parity assertion over random parameters catches a huge class of sign errors, discounting errors, and \(d_1/d_2\) mix-ups.
- Asserting
price == 12.34on a Monte Carlo estimate; it is random - assert it is within a few standard errors. - Testing only the ‘happy path’ and never \(T=0\), \(\sigma=0\), empty arrays, or extreme strikes.
- Choosing a tolerance tighter than the method’s own error (e.g. 1e-12 on a 1e-2-accurate MC price).
- Deleting a test because it’s ‘annoying’ instead of fixing the code - that test was finding a real bug.
- Letting a function return
nansilently instead of raising or handling the boundary; nan spreads through sums. - No regression test after a fix, so the same bug returns in three months.
- Write the failing test first (it reproduces the bug), then fix until green - that is the regression discipline.
- Prefer invariants (parity, monotonicity, non-negativity) over hard-coded numbers; they generalize across inputs.
- For Monte Carlo, test the estimator’s standard error, e.g. |estimate−truth| < 3×SE, not exact equality.
- Keep one tiny deterministic dataset in the repo so tests are fast and reproducible.
- Assert on shapes and dtypes too; a (T,1) vs (T,) mismatch is a classic silent broadcasting bug.
Interactive: write the test that catches the boundary bug
The starter contains a corrected pricer plus a parity check. Implement the boundary handling and the property test so all assertions pass.
Knowledge Check
abs(a-b)<tol instead of a==b for computed reals?Practical Exercise
You inherit a function impl_vol(price,S,K,r,T) that inverts Black–Scholes for implied volatility. Design a test suite: name one oracle test, two boundary tests, and one property test, each with the specific input and expected behavior.
Oracle: price an option with a known \(\sigma=0.2\) via bs_call, feed that price back in, assert impl_vol(...) recovers 0.20 to a tolerance matching the root-finder (e.g. 1e-6).
Boundary 1: price below intrinsic value (arbitrage input) - assert it raises or returns NaN rather than a bogus vol.
Boundary 2: price equal to a deep-ITM intrinsic with tiny time value - assert vol is small and finite (no divide-by-zero or non-convergence).
Property: monotonicity - for two prices \(p_1<p_2\) on the same option, impl_vol(p1)<impl_vol(p2), since vega is positive (call price increases in \(\sigma\)). Test over random parameter draws.
Lesson Summary
Retrieval Practice
Close the lesson and answer from memory before checking. This is deliberate, effortful recall - the single highest-yield study action.
A: \(|a-b|\le\text{atol}+\text{rtol}|b|\). Float == is wrong because IEEE-754 rounds each operation, so mathematically equal computations differ in the low bits.
A: Boundary: \(T=0\) (or \(\sigma=0\)) must return discounted intrinsic \(\max(S-Ke^{-rT},0)\), not NaN. Property: put-call parity \(C-P=S-Ke^{-rT}\) must hold for all inputs.
Completion Checklist
- I can explain the core ideas in my own words
- I worked the derivations/examples by hand
- I completed the interactive workbench(es)
- I passed the knowledge check
Source References
This lesson synthesizes and paraphrases concepts from the sources below. No copyrighted text, problem sets, or solutions are reproduced. Return to the originals for full depth.
- The Pragmatic Programmer (Thomas & Hunt, 2nd/20th Anniv. ed., 2019) current - Ch. 4, 6 - Testing to find silent bugs, regression tests, assertions, and ‘design by contract’ invariants.
- Monte Carlo Methods in Financial Engineering (Paul Glasserman, 2004) foundational - Ch. 1 - Standard error of Monte Carlo estimators; tolerances that respect estimator variance.