import json
import math
import os
import random
import sys
import mpmath
import QuantLib as ql
mpmath.mp.dps = 50
random.seed(42)
SCALE = 10**12
BENCHMARK_DIR = os.path.join(os.path.dirname(__file__), '..', 'benchmark')
N_SAMPLES = 5000
def load_vectors(filename):
path = os.path.join(BENCHMARK_DIR, filename)
with open(path, 'r') as f:
data = json.load(f)
return data.get('vectors', data)
def sig_figs_agreement(a, b):
if a == 0 and b == 0:
return 16.0 magnitude = max(abs(a), abs(b))
if magnitude == 0:
return 16.0
diff = abs(a - b)
if diff == 0:
return 16.0 ratio = diff / magnitude
if ratio <= 0:
return 16.0
sf = -math.log10(ratio)
return max(sf, 0.0)
def mpmath_bs(S, K, r, sigma, T):
S, K, r, sigma, T = [mpmath.mpf(x) for x in [S, K, r, sigma, T]]
vol_sqrt_t = sigma * mpmath.sqrt(T)
if vol_sqrt_t < mpmath.mpf('1e-15'):
return None
d1 = (mpmath.log(S / K) + (r + sigma**2 / 2) * T) / vol_sqrt_t
d2 = d1 - vol_sqrt_t
npdf_d1 = mpmath.exp(-d1**2 / 2) / mpmath.sqrt(2 * mpmath.pi)
ncdf_d1 = mpmath.ncdf(d1)
ncdf_d2 = mpmath.ncdf(d2)
disc = mpmath.exp(-r * T)
call = float(S * ncdf_d1 - K * disc * ncdf_d2)
put = float(K * disc * (1 - ncdf_d2) - S * (1 - ncdf_d1))
delta_call = float(ncdf_d1)
delta_put = float(ncdf_d1 - 1)
gamma = float(npdf_d1 / (S * sigma * mpmath.sqrt(T)))
vega = float(S * npdf_d1 * mpmath.sqrt(T))
theta_call = float(-(S * npdf_d1 * sigma) / (2 * mpmath.sqrt(T)) - r * K * disc * ncdf_d2)
theta_put = float(-(S * npdf_d1 * sigma) / (2 * mpmath.sqrt(T)) + r * K * disc * (1 - ncdf_d2))
rho_call = float(K * T * disc * ncdf_d2)
rho_put = float(-K * T * disc * (1 - ncdf_d2))
return {
'call': call, 'put': put,
'call_delta': delta_call, 'put_delta': delta_put,
'gamma': gamma, 'vega': vega,
'call_theta': theta_call, 'put_theta': theta_put,
'call_rho': rho_call, 'put_rho': rho_put,
}
def quantlib_bs(S, K, r, sigma, T):
forward = S * math.exp(r * T)
stddev = sigma * math.sqrt(T)
discount = math.exp(-r * T)
call_payoff = ql.PlainVanillaPayoff(ql.Option.Call, K)
put_payoff = ql.PlainVanillaPayoff(ql.Option.Put, K)
cc = ql.BlackCalculator(call_payoff, forward, stddev, discount)
pc = ql.BlackCalculator(put_payoff, forward, stddev, discount)
return {
'call': cc.value(),
'put': pc.value(),
'call_delta': cc.delta(S),
'put_delta': pc.delta(S),
'gamma': cc.gamma(S),
'vega': cc.vega(T),
'call_theta': cc.theta(S, T),
'put_theta': pc.theta(S, T),
'call_rho': cc.rho(T),
'put_rho': pc.rho(T),
}
def solmath_from_vector(v):
return {
'call': int(v['call']) / SCALE,
'put': int(v['put']) / SCALE,
'call_delta': int(v['call_delta']) / SCALE,
'put_delta': int(v['put_delta']) / SCALE,
'gamma': int(v['gamma']) / SCALE,
'vega': int(v['vega']) / SCALE,
'call_theta': int(v['call_theta']) / SCALE,
'put_theta': int(v['put_theta']) / SCALE,
'call_rho': int(v['call_rho']) / SCALE,
'put_rho': int(v['put_rho']) / SCALE,
}
GREEKS = ['call', 'put', 'call_delta', 'put_delta', 'gamma',
'vega', 'call_theta', 'put_theta', 'call_rho', 'put_rho']
def main():
print(f"SolMath vs QuantLib Cross-Validation")
print(f"QuantLib {ql.__version__} (BlackCalculator, f64)")
print(f"mpmath at {mpmath.mp.dps} decimal digits")
print(f"Sampling {N_SAMPLES} vectors from prod_bs_full_hp_vectors.json\n")
vectors = load_vectors('prod_bs_full_hp_vectors.json')
print(f"Loaded {len(vectors)} vectors")
samples = random.sample(vectors, min(N_SAMPLES, len(vectors)))
print(f"Selected {len(samples)} random samples\n")
ql_vs_mp = {g: [] for g in GREEKS} sm_vs_ql = {g: [] for g in GREEKS} sm_vs_mp = {g: [] for g in GREEKS}
errors = 0
for i, v in enumerate(samples):
if i % 1000 == 0 and i > 0:
print(f" processed {i}/{len(samples)}...")
S = int(v['s']) / SCALE
K = int(v['k']) / SCALE
r = int(v['r']) / SCALE
sigma = int(v['sigma']) / SCALE
T = int(v['t']) / SCALE
try:
mp_vals = mpmath_bs(S, K, r, sigma, T)
ql_vals = quantlib_bs(S, K, r, sigma, T)
except Exception as e:
errors += 1
continue
if mp_vals is None:
errors += 1
continue
sm_vals = solmath_from_vector(v)
for g in GREEKS:
mp_v = mp_vals[g]
ql_v = ql_vals[g]
sm_v = sm_vals[g]
if abs(mp_v) < 1e-15:
continue
ql_vs_mp[g].append(sig_figs_agreement(ql_v, mp_v))
sm_vs_ql[g].append(sig_figs_agreement(sm_v, ql_v))
sm_vs_mp[g].append(sig_figs_agreement(sm_v, mp_v))
if errors:
print(f" ({errors} vectors skipped due to errors)\n")
def stats(vals):
if not vals:
return {'min': 0, 'median': 0, 'p05': 0, 'mean': 0, 'n': 0}
vals_sorted = sorted(vals)
n = len(vals_sorted)
return {
'min': vals_sorted[0],
'median': vals_sorted[n // 2],
'p05': vals_sorted[int(n * 0.05)],
'mean': sum(vals) / n,
'n': n,
}
print("=" * 90)
print(" QuantLib vs mpmath (f64 vs 50-digit reference)")
print("=" * 90)
print(f"{'Greek':<15} {'n':>6} {'Min SF':>8} {'P05 SF':>8} {'Median SF':>10} {'Mean SF':>8}")
print("-" * 90)
for g in GREEKS:
s = stats(ql_vs_mp[g])
print(f"{g:<15} {s['n']:>6} {s['min']:>8.1f} {s['p05']:>8.1f} {s['median']:>10.1f} {s['mean']:>8.1f}")
print()
print("=" * 90)
print(" SolMath HP (fixed-point) vs QuantLib (f64)")
print("=" * 90)
print(f"{'Greek':<15} {'n':>6} {'Min SF':>8} {'P05 SF':>8} {'Median SF':>10} {'Mean SF':>8}")
print("-" * 90)
for g in GREEKS:
s = stats(sm_vs_ql[g])
print(f"{g:<15} {s['n']:>6} {s['min']:>8.1f} {s['p05']:>8.1f} {s['median']:>10.1f} {s['mean']:>8.1f}")
print()
print("=" * 90)
print(" SolMath HP (fixed-point) vs mpmath (50-digit reference)")
print("=" * 90)
print(f"{'Greek':<15} {'n':>6} {'Min SF':>8} {'P05 SF':>8} {'Median SF':>10} {'Mean SF':>8}")
print("-" * 90)
for g in GREEKS:
s = stats(sm_vs_mp[g])
print(f"{g:<15} {s['n']:>6} {s['min']:>8.1f} {s['p05']:>8.1f} {s['median']:>10.1f} {s['mean']:>8.1f}")
report = []
report.append("# SolMath vs QuantLib Cross-Validation\n")
report.append(f"**QuantLib {ql.__version__}** (BlackCalculator, IEEE 754 f64)")
report.append(f"| **mpmath** ({mpmath.mp.dps}-digit reference)")
report.append(f"| **SolMath bs_full_hp** (i128 fixed-point)\n")
report.append(f"{len(samples)} randomly sampled production vectors ")
report.append(f"from {len(vectors)} total (seed=42)\n")
report.append("Agreement measured in **significant figures** ")
report.append("(-log10 of relative difference). ")
report.append("Higher is better; 12.0 = agreement to 12 decimal digits.\n")
for title, data in [
("QuantLib vs mpmath (f64 vs 50-digit reference)", ql_vs_mp),
("SolMath HP vs QuantLib (fixed-point vs f64 industry standard)", sm_vs_ql),
("SolMath HP vs mpmath (fixed-point vs 50-digit reference)", sm_vs_mp),
]:
report.append(f"\n## {title}\n")
report.append(f"| Greek | n | Min SF | P05 SF | Median SF | Mean SF |")
report.append(f"|-------|---|--------|--------|-----------|---------|")
for g in GREEKS:
s = stats(data[g])
report.append(
f"| {g} | {s['n']} | {s['min']:.1f} | {s['p05']:.1f} "
f"| {s['median']:.1f} | {s['mean']:.1f} |"
)
report.append("\n## Interpretation\n")
report.append("- **QuantLib vs mpmath** shows the f64 precision ceiling (~15.9 sig figs max).")
report.append(" Any disagreement here is IEEE 754 rounding, not a bug.")
report.append("- **SolMath HP vs QuantLib** is the headline number: how SolMath compares")
report.append(" to the industry standard. Agreement to 10+ sig figs means the fixed-point")
report.append(" implementation is practically indistinguishable from QuantLib.")
report.append("- **SolMath HP vs mpmath** shows absolute accuracy of the fixed-point path.")
report.append(" This is bounded by the i128/SCALE=1e12 representation (max ~12 sig figs).")
report.append("")
report_path = os.path.join(BENCHMARK_DIR, 'QUANTLIB_CROSSCHECK.md')
with open(report_path, 'w') as f:
f.write('\n'.join(report))
print(f"\nWrote {report_path}")
if __name__ == '__main__':
main()