@@ -52,6 +52,45 @@ fn is_degenerate(v: f64, t: f64) -> bool {
5252 t <= 0.0 || v <= 0.0
5353}
5454
55+ /// Return true if spot or strike make Black-Scholes degenerate. A
56+ /// non-positive spot or strike makes `(s / x).ln()` non-finite (and
57+ /// `x == 0.0` an outright divide-by-zero), so the bundle path treats
58+ /// these as degenerate to keep every Greek finite.
59+ #[ inline]
60+ fn is_price_degenerate ( s : f64 , x : f64 ) -> bool {
61+ !is_positive ( s) || !is_positive ( x)
62+ }
63+
64+ /// Return true only for a strictly positive, finite-comparable value.
65+ /// `NaN` is not positive, so it reports `false` and routes to the
66+ /// degenerate path instead of poisoning downstream arithmetic.
67+ #[ inline]
68+ fn is_positive ( value : f64 ) -> bool {
69+ value > 0.0
70+ }
71+
72+ /// Reject a non-positive spot or strike at a fallible public entry point.
73+ ///
74+ /// Black-Scholes is undefined for `spot <= 0` or `strike <= 0`: the
75+ /// `(spot / strike).ln()` term goes non-finite and `strike == 0` is an
76+ /// outright divide-by-zero. Rejecting here keeps every SDK / CLI / MCP
77+ /// surface from serialising NaN or `null` Greeks.
78+ // Reason: s, x are the standard Black-Scholes spot/strike parameter names.
79+ #[ allow( clippy:: many_single_char_names) ]
80+ fn reject_nonpositive_price ( s : f64 , x : f64 ) -> Result < ( ) , crate :: tdbe:: Error > {
81+ if !is_positive ( s) {
82+ return Err ( crate :: tdbe:: Error :: Config ( format ! (
83+ "spot must be strictly positive, got {s}"
84+ ) ) ) ;
85+ }
86+ if !is_positive ( x) {
87+ return Err ( crate :: tdbe:: Error :: Config ( format ! (
88+ "strike must be strictly positive, got {x}"
89+ ) ) ) ;
90+ }
91+ Ok ( ( ) )
92+ }
93+
5594/// Standard normal CDF approximation (Zelen & Severo, 1964).
5695///
5796/// Uses Horner's method for polynomial evaluation: 4 fused multiply-adds instead
@@ -451,7 +490,9 @@ pub fn dual_gamma(s: f64, x: f64, v: f64, r: f64, q: f64, t: f64) -> f64 {
451490/// # Errors
452491///
453492/// Returns [`crate::greeks::Error::Config`] if `right` is not one of the accepted
454- /// forms or resolves to `both`/`*`. Strict-parse failures from
493+ /// forms or resolves to `both`/`*`, or if `spot`/`strike` is non-positive
494+ /// (Black-Scholes requires `spot > 0` and `strike > 0`; `strike == 0` is an
495+ /// outright divide-by-zero). Strict-parse failures from
455496/// [`crate::tdbe::right::parse_right_strict`] surface here directly.
456497// Reason: s, x, r, q, t are standard Black-Scholes parameter names.
457498#[ allow( clippy:: many_single_char_names) ]
@@ -471,6 +512,7 @@ pub fn implied_volatility(
471512 "option right '{right}' resolves to 'both' but a single side is required"
472513 ) )
473514 } ) ?;
515+ reject_nonpositive_price ( s, x) ?;
474516 if t <= 0.0 || option_price <= 0.0 {
475517 return Ok ( ( 0.0 , 0.0 ) ) ;
476518 }
@@ -618,7 +660,9 @@ pub struct GreeksResult {
618660/// # Errors
619661///
620662/// Returns [`crate::greeks::Error::Config`] if `right` is not one of the accepted
621- /// forms or resolves to `both`/`*`. Strict-parse failures from
663+ /// forms or resolves to `both`/`*`, or if `spot`/`strike` is non-positive
664+ /// (Black-Scholes requires `spot > 0` and `strike > 0`; `strike == 0` is an
665+ /// outright divide-by-zero). Strict-parse failures from
622666/// [`crate::tdbe::right::parse_right_strict`] surface here directly.
623667// Reason: s, x, r, q, t are standard Black-Scholes parameter names.
624668// Reason: 23-Greek computation cannot be meaningfully split without duplicating intermediates.
@@ -643,6 +687,7 @@ pub fn all_greeks(
643687 "option right '{right}' resolves to 'both' but a single side is required"
644688 ) )
645689 } ) ?;
690+ reject_nonpositive_price ( s, x) ?;
646691
647692 // Inline the IV solver to keep the `right` parse at this layer (avoids
648693 // a second parse that `implied_volatility(&str)` would otherwise do).
@@ -694,6 +739,12 @@ pub fn all_greeks(
694739/// negative tenor) every Greek field is `0.0` except `value`,
695740/// which is the intrinsic value. Mirrors [`all_greeks`]'s
696741/// degenerate-branch semantics.
742+ ///
743+ /// A non-positive (or non-finite) spot or strike is also degenerate:
744+ /// `(spot / strike).ln()` is non-finite and `strike == 0` divides by
745+ /// zero, so the whole bundle is returned all-zero rather than emitting
746+ /// NaN. The fallible [`all_greeks`] / [`implied_volatility`] entry points
747+ /// reject these inputs up front; this infallible helper clamps them.
697748// Reason: s, x, v, r, q, t are standard Black-Scholes parameter names.
698749#[ allow(
699750 clippy:: many_single_char_names,
@@ -711,6 +762,37 @@ pub fn compute_full_bundle_with_iv(
711762 t : f64 ,
712763 is_call : bool ,
713764) -> GreeksResult {
765+ // Guard: a non-positive/non-finite spot or strike makes the whole
766+ // surface non-finite (`(s / x).ln()`, and `x == 0` divides by zero),
767+ // so return an all-zero bundle rather than emitting NaN.
768+ if is_price_degenerate ( s, x) {
769+ return GreeksResult {
770+ value : 0.0 ,
771+ delta : 0.0 ,
772+ gamma : 0.0 ,
773+ theta : 0.0 ,
774+ vega : 0.0 ,
775+ rho : 0.0 ,
776+ iv : v,
777+ iv_error : 0.0 ,
778+ vanna : 0.0 ,
779+ charm : 0.0 ,
780+ vomma : 0.0 ,
781+ veta : 0.0 ,
782+ vera : 0.0 ,
783+ speed : 0.0 ,
784+ zomma : 0.0 ,
785+ color : 0.0 ,
786+ ultima : 0.0 ,
787+ d1 : 0.0 ,
788+ d2 : 0.0 ,
789+ dual_delta : 0.0 ,
790+ dual_gamma : 0.0 ,
791+ epsilon : 0.0 ,
792+ lambda : 0.0 ,
793+ } ;
794+ }
795+
714796 // Guard: if vol or time is degenerate, return all zeros (except value = intrinsic).
715797 if is_degenerate ( v, t) {
716798 return GreeksResult {
@@ -1248,6 +1330,81 @@ mod tests {
12481330 assert ! ( bundle. value. is_finite( ) ) ;
12491331 }
12501332
1333+ // -- Non-positive spot / strike rejection --
1334+
1335+ #[ test]
1336+ fn all_greeks_errors_on_nonpositive_spot ( ) {
1337+ for spot in [ 0.0 , -1.0 , -100.0 ] {
1338+ let err = all_greeks ( spot, 100.0 , 0.05 , 0.01 , 0.25 , 5.0 , "C" ) . unwrap_err ( ) ;
1339+ assert ! ( matches!( err, crate :: tdbe:: Error :: Config ( _) ) , "spot={spot}" ) ;
1340+ assert ! (
1341+ err. to_string( ) . contains( "spot must be strictly positive" ) ,
1342+ "spot={spot}: {err}"
1343+ ) ;
1344+ }
1345+ }
1346+
1347+ #[ test]
1348+ fn all_greeks_errors_on_nonpositive_strike ( ) {
1349+ for strike in [ 0.0 , -1.0 , -100.0 ] {
1350+ let err = all_greeks ( 100.0 , strike, 0.05 , 0.01 , 0.25 , 5.0 , "C" ) . unwrap_err ( ) ;
1351+ assert ! (
1352+ matches!( err, crate :: tdbe:: Error :: Config ( _) ) ,
1353+ "strike={strike}"
1354+ ) ;
1355+ assert ! (
1356+ err. to_string( ) . contains( "strike must be strictly positive" ) ,
1357+ "strike={strike}: {err}"
1358+ ) ;
1359+ }
1360+ }
1361+
1362+ #[ test]
1363+ fn implied_volatility_errors_on_nonpositive_spot ( ) {
1364+ for spot in [ 0.0 , -1.0 ] {
1365+ let err = implied_volatility ( spot, 100.0 , 0.05 , 0.01 , 0.25 , 5.0 , "C" ) . unwrap_err ( ) ;
1366+ assert ! ( matches!( err, crate :: tdbe:: Error :: Config ( _) ) , "spot={spot}" ) ;
1367+ assert ! (
1368+ err. to_string( ) . contains( "spot must be strictly positive" ) ,
1369+ "spot={spot}: {err}"
1370+ ) ;
1371+ }
1372+ }
1373+
1374+ #[ test]
1375+ fn implied_volatility_errors_on_nonpositive_strike ( ) {
1376+ for strike in [ 0.0 , -1.0 ] {
1377+ let err = implied_volatility ( 100.0 , strike, 0.05 , 0.01 , 0.25 , 5.0 , "C" ) . unwrap_err ( ) ;
1378+ assert ! (
1379+ matches!( err, crate :: tdbe:: Error :: Config ( _) ) ,
1380+ "strike={strike}"
1381+ ) ;
1382+ assert ! (
1383+ err. to_string( ) . contains( "strike must be strictly positive" ) ,
1384+ "strike={strike}: {err}"
1385+ ) ;
1386+ }
1387+ }
1388+
1389+ #[ test]
1390+ fn compute_full_bundle_zeros_on_nonpositive_spot_or_strike ( ) {
1391+ // The infallible bundle path must clamp non-positive spot/strike to
1392+ // an all-zero bundle rather than emitting NaN/Inf in any field.
1393+ for ( s, x) in [ ( 0.0 , 100.0 ) , ( -1.0 , 100.0 ) , ( 100.0 , 0.0 ) , ( 100.0 , -1.0 ) ] {
1394+ let bundle = compute_full_bundle_with_iv ( s, x, 0.20 , 0.05 , 0.01 , 0.25 , true ) ;
1395+ assert_eq ! ( bundle. value, 0.0 , "value s={s} x={x}" ) ;
1396+ assert_eq ! ( bundle. delta, 0.0 , "delta s={s} x={x}" ) ;
1397+ assert_eq ! ( bundle. gamma, 0.0 , "gamma s={s} x={x}" ) ;
1398+ assert_eq ! ( bundle. theta, 0.0 , "theta s={s} x={x}" ) ;
1399+ assert_eq ! ( bundle. vega, 0.0 , "vega s={s} x={x}" ) ;
1400+ assert_eq ! ( bundle. d1, 0.0 , "d1 s={s} x={x}" ) ;
1401+ assert_eq ! ( bundle. d2, 0.0 , "d2 s={s} x={x}" ) ;
1402+ // Every field stays finite (no NaN, no Inf).
1403+ assert_finite ( bundle. dual_gamma , "dual_gamma" ) ;
1404+ assert_finite ( bundle. lambda , "lambda" ) ;
1405+ }
1406+ }
1407+
12511408 // ---------------------------------------------------------------------------
12521409 // Property-based tests
12531410 // ---------------------------------------------------------------------------
0 commit comments