Sitelet https://github.com/domokane/FinancePy/pull/276
Skip to content

Fix negative-volatility guard silently skipped in FX option greeks - #276

Merged
domokane merged 1 commit into
domokane:masterfrom
heykav:fix/fx-vanilla-option-negative-vol-guard
Sep 24, 2026
Merged

domokane merged 1 commit into
domokane:masterfrom
heykav:fix/fx-vanilla-option-negative-vol-guard

Conversation

@heykav

@heykav heykav commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Bug

FXVanillaOption.gamma, .vega, and .theta in financepy/products/fx/fx_vanilla_option.py each guard against a negative volatility with:

if np.any(volatility) < 0.0:
    raise FinError("Volatility should not be negative.")

This checks the truth value of np.any(volatility) (a Python bool) against 0.0, not each element of volatility against 0.0. Since a bool is never < 0.0, the condition is always False, so the guard can never raise. A negative volatility silently flows through into the pricing math and produces a wrong (non-error) greek instead of raising, while the sibling method delta() (which correctly uses np.any(v < 0.0)) does raise for the same input — an inconsistency within the same class.

Root cause

Operator precedence: np.any(x) < 0.0 reduces first, then compares the reduced boolean; the intended check is np.any(x < 0.0), which compares element-wise first, then reduces.

The identical pattern existed in financepy/utils/helpers.py:input_time, in the ndarray branch's check that a curve date isn't before the curve's value date (np.any(t) < 0), with the same effect: negative time-in-the-past inputs on an array of dates silently pass instead of raising.

Fix

  • fx_vanilla_option.py: changed all three occurrences to np.any(volatility < 0.0) / np.any(vol < 0.0).
  • utils/helpers.py: changed np.any(t) < 0 to np.any(t < 0).

Verification

  • Added test_negative_volatility_raises in tests/unit/test_FinFXVanillaOption.py, asserting gamma/vega/theta raise FinError for a negative-volatility BlackScholes model.
  • Red/green: reverted the fix and confirmed the new test fails against the old code; reapplied the fix and confirmed it passes.
  • Ran the full unit test suite (pytest tests) before and after: 1050 passing before (plus the new test failing), 1051 passing after — no regressions, and existing positive-volatility results for gamma/vega/theta are numerically unchanged (spot-checked against test_vega_theta's Hull example values).

FXVanillaOption.gamma/vega/theta guarded against negative volatility with
"if np.any(volatility) < 0.0:", which checks the truth value of
np.any(volatility) (a bool) against 0.0 rather than checking each element
of volatility against 0.0. Since a bool is never < 0.0, the guard could
never raise, so a negative volatility silently produced a (wrong,
non-error) greek instead of raising FinError as the analogous check in
delta() correctly does.

Fixed by comparing element-wise before reducing: np.any(volatility < 0.0).
Applied the same fix to the identical pattern in
utils/helpers.py:input_time (np.any(t) < 0 -> np.any(t < 0)), which had
the same bug for its curve-date-in-the-past check on ndarray inputs.

Added a regression test (test_negative_volatility_raises) confirming
gamma/vega/theta now raise FinError for negative volatility. Verified
red/green: the new test fails on the old code and passes with the fix;
full unit test suite (1051 tests) passes with the fix applied, positive-
volatility results are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@domokane
domokane merged commit cc14000 into domokane:master Sep 24, 2026
2 checks passed
@domokane

Copy link
Copy Markdown
Owner

Thanks.

@domokane

Copy link
Copy Markdown
Owner

Thanks

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.

2 participants