Skip to content

fix: fail arbitrage scans that cannot evaluate every grid point - #134

Merged
pandashark merged 1 commit into
mainfrom
richardamaya/pan-135-api-contract-gaps-from-roborev-api-design-review-4228
Aug 20, 2026
Merged

fix: fail arbitrage scans that cannot evaluate every grid point#134
pandashark merged 1 commit into
mainfrom
richardamaya/pan-135-api-contract-gaps-from-roborev-api-design-review-4228

Conversation

@pandashark

Copy link
Copy Markdown
Contributor

Closes item 1 of PAN-135. The other six findings stay open on the ticket.

Problem

scan_density and scan_g both did Err(_) => continue on a density failure and still returned Ok. A caller could not tell "every grid point was checked and none violated" from "most points failed to evaluate and were dropped".

The crate's own test suite contained the proof: is_arbitrage_free_skips_density_errors built a smile with negative total variance at every strike and asserted it reported arbitrage-free.

scan_g was the sharper case. It only evaluates density at points already flagged by g(k) < -BUTTERFLY_G_TOL, so the skip discarded violations that had already been detected rather than merely leaving points unchecked. Since gatheral_g returns NEG_INFINITY when w <= 0, a smile with negative variance everywhere flagged every point and then dropped every one.

Change

Both helpers now propagate, wrapping the cause with the strike that failed:

let d = density(strike).map_err(|e| error::VolSurfError::NumericalError {
    message: format!("arbitrage scan failed to evaluate density at strike {strike}: {e}"),
})?;

Ok(ArbitrageReport) now means the whole configured grid was covered.

Why not record scan coverage instead

The ticket offered that alternative. ArbitrageReport has public fields and no #[non_exhaustive], so adding points_attempted / points_evaluated breaks every struct literal — including the doctest in arbitrage.rs and the construction in spline.rs. That belongs with the other breaking items the review already deferred to v3.0.

No type signatures change here, and VolSurfError already has a NumericalError variant, so no new variant either.

Blast radius

Near-zero for models built through validated constructors:

  • SSVI / eSSVI — unreachable; w > 0 for all params new accepts.
  • SABR — unreachable since the correction floor clamp in hagan_implied_vol.
  • SVI — needs min_variance == 0.0 and a grid point landing exactly on the argmin.
  • SplineSmile — has the one genuinely live failure mode (negative interpolated variance between knots), and its is_arbitrage_free override already propagated with ?.

The path is reachable mainly by bypassing constructor validation, which is exactly what the inverted test does.

Tests

  • is_arbitrage_free_skips_density_errorsis_arbitrage_free_errors_on_density_failure, now asserting Err and that the message names a strike.
  • New scan_density_fails_when_a_grid_point_cannot_be_evaluated covering the scan_density side, which SVI's g-function path never exercised.
  • Everything else unchanged: 891 lib + 90 integration Rust, 258 Python, 78 WASM, clippy and rustfmt clean, cargo doc clean under -D warnings.

Not in scope

SplineSmile overrides is_arbitrage_free but not is_arbitrage_free_with, so it scans absolute strike space over its knot range in one method and log-moneyness via the trait default in the other, ignoring ArbitrageScanConfig in both. After this change the two agree on error semantics but still disagree on domain. Separate defect — aligning it changes which violations spline reports.

Both scan helpers swallowed density failures and still returned Ok, so a
report could not distinguish "no violations found" from "most points were
never checked". A smile with non-positive total variance reported as
arbitrage-free.

scan_g was the sharper case: it only evaluates density at points already
flagged by g(k) < -tol, so the skip discarded violations that had already
been detected rather than merely leaving points unchecked.

Both now return NumericalError naming the strike that failed, so Ok means
the whole configured grid was covered. Models built through their validated
constructors are unaffected — SSVI and eSSVI cannot reach the path at all,
SABR has been clamped since the correction floor was added, and SVI needs
its variance minimum to sit exactly on a grid point.

The SABR doc claiming failed points are skipped was already stale before
this change; it now states the guarantee.

Refs PAN-135 (item 1 of 7).
@pandashark
pandashark merged commit 5dda8b1 into main Aug 20, 2026
7 checks passed
@pandashark
pandashark deleted the richardamaya/pan-135-api-contract-gaps-from-roborev-api-design-review-4228 branch August 20, 2026 02:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant