"""Realistic bugs, written as edits to pricer.py. Each mutant replaces one exact piece of source text. Building it fails if that text is missing or not unique, so a mutant cannot silently stop doing anything. """ import types from collections import namedtuple from pathlib import Path Mutant = namedtuple("Mutant", "id title why before after") SOURCE = (Path(__file__).parent / "pricer.py").read_text() MUTANTS = [ Mutant("d2-time", "d2 uses time instead of its square root", "Identical when time is exactly one year.", "d2 = d1 - vol * sqrt(time)", "d2 = d1 - vol * time"), Mutant("d1-half-sign", "Wrong sign on the convexity term in d1", "An easy slip when copying the formula.", "(rate - div + 0.5 * vol * vol)", "(rate - div - 0.5 * vol * vol)"), Mutant("d1-no-carry", "Dividend yield left out of d1", "Harmless whenever the test uses a zero dividend.", "(rate - div + 0.5 * vol * vol)", "(rate + 0.5 * vol * vol)"), Mutant("log-inverted", "Log-moneyness inverted", "Harmless at the money, where the log is zero.", "log(spot / strike)", "log(strike / spot)"), Mutant("simple-discount", "Simple discounting on the strike", "Close to the right number for small rates and short maturities.", "strike * exp(-rate * time)", "strike / (1.0 + rate * time)"), Mutant("no-dividend-carry", "Spot not carried at the dividend yield", "Harmless whenever the test uses a zero dividend.", "return spot * exp(-div * time), strike", "return spot, strike"), Mutant("call-d1-for-d2", "Call uses N(d1) on both legs", "A copy and paste error in one term.", "return carried * N(d1) - discounted * N(d2)", "return carried * N(d1) - discounted * N(d1)"), Mutant("put-sign", "Put uses N(d2) instead of N(-d2)", "A dropped minus sign.", "return discounted * N(-d2) - carried * N(-d1)", "return discounted * N(d2) - carried * N(-d1)"), Mutant("delta-no-carry", "Delta ignores the dividend yield", "The price is right, only the Greek is wrong.", "return exp(-div * time) * N(d1)", "return N(d1)"), Mutant("delta-d2", "Delta uses d2", "A common confusion, since N(d2) is the exercise probability.", "d1, _ = d1_d2(spot, strike, rate, div, vol, time)\n return exp(-div * time) * N(d1)", "_, d1 = d1_d2(spot, strike, rate, div, vol, time)\n return exp(-div * time) * N(d1)"), Mutant("gamma-no-vol", "Gamma missing the volatility term", "The price is right, only the Greek is wrong.", "n(d1) / (spot * vol * sqrt(time))", "n(d1) / (spot * sqrt(time))"), Mutant("gamma-time", "Gamma uses time instead of its square root", "Identical when time is exactly one year.", "n(d1) / (spot * vol * sqrt(time))", "n(d1) / (spot * vol * time)"), Mutant("vega-no-root", "Vega missing the square root of time", "Identical when time is exactly one year.", "n(d1) * sqrt(time)", "n(d1)"), Mutant("degenerate-undiscounted", "Zero-volatility call ignores discounting", "Only reached at the edge of the input space.", "return max(carried - discounted, 0.0)", "return max(spot - strike, 0.0)"), Mutant("degenerate-guard-removed", "Zero-volatility guard removed", "Only reached at the edge of the input space.", "return vol * sqrt(time) < 1e-12", "return False"), Mutant("implied-too-few-steps", "Implied volatility search stops early", "The answer looks plausible and is slightly off.", "for _ in range(200):", "for _ in range(6):"), ] def build(mutant=None): """Returns the pricer as a fresh module, with one mutant applied if given.""" source = SOURCE if mutant is not None: count = source.count(mutant.before) if count != 1: raise ValueError("{} matched {} times".format(mutant.id, count)) source = source.replace(mutant.before, mutant.after) module = types.ModuleType("pricer_under_test") exec(compile(source, "pricer_under_test", "exec"), module.__dict__) return module