M1: Eliminate .unwrap()/.expect() from src/pricing/ (#318)#350
Merged
joaquinbejar merged 2 commits intomainfrom Apr 17, 2026
Merged
M1: Eliminate .unwrap()/.expect() from src/pricing/ (#318)#350joaquinbejar merged 2 commits intomainfrom
joaquinbejar merged 2 commits intomainfrom
Conversation
Eliminate every production .unwrap() / .expect() under src/pricing/. Closes the per-file refactor steps C2-C5 of issue #318 (M1 Panic-Free Core). Per-file site counts: - utils.rs: 11 — sqrt + from_f64 + Normal::new + chain unwraps now ?-propagated via PricingError::method_error / DecimalError variants. - telegraph.rs: 9 — 1 in-place unwrap_or(0.0) + tracing::warn! for next_state -> i8 (probability provably in [0,1]); the rest ? through PricingError::method_error. - payoff.rs: 6 — 4 doctests rewrapped in `# fn run() -> Result<(), Box<dyn std::error::Error>>`; 2 in-place unwrap_or(0.0) + tracing::warn! for standard_payoff -> f64 (max of two finite Decimals is provably finite). - american.rs: 5 — doctest examples wrapped in `# fn run`. - barrier.rs: 4 — 2 from_f64 + 2 sqrt → ok_or_else with method_error. - asian.rs: 4 — sqrt + Positive::new sites mapped via map_err / ?. - monte_carlo.rs: 3 — to_f64 + from_usize → ok_or_else + ?. - cliquet.rs: 3 — partial_cmp guarded with NaN check + unwrap_or(Equal), 2 from_f64 → ?. - binomial_model.rs: 2 — Vec collect rewritten to collect::<Result<Vec<_>, _>>()?, plus 1 doctest wrap. - compound.rs: 1 — Positive::new(2.0) → map_err + ?. Also: the #[should_panic] test test_probability_keep_under_strike_zero_volatility (utils.rs:647) became assert!(... .is_err()) since the panic is now a typed DecimalError — exactly the goal of this issue. Public API impact: NONE. Both F-blockers (telegraph::next_state -> i8 and payoff::standard_payoff -> f64) used in-place unwrap_or(safe_default) + tracing::warn! per the plan's Option A. Trait surfaces (Payoff, PayoffInfo, Profit) and all `pub fn` signatures preserved. Build: cargo build --all-targets --all-features clean. Clippy: cargo clippy --all-targets --all-features --workspace -- -D warnings clean. Tests: cargo test --lib pricing:: 240 passed; 0 failed. Doctests: 203 passed; 0 failed. ACCEPTANCE GREP — production unwrap/expect under src/pricing/: 0.
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR advances the project’s “panic-free core” milestone by removing .unwrap() / .expect() usage from production code under src/pricing/, converting previously panicking numeric conversions and Option-returning math ops into typed error paths (and updating doctests accordingly).
Changes:
- Replaced
unwrap()/expect()in pricing computations (e.g.,Decimal::sqrt,Decimal::{from_f64,to_f64}, distribution construction) with?,ok_or_else, and typedPricingError/DecimalErrorpropagation. - Updated doctests/examples to compile under fallible APIs by wrapping examples in
# fn run() -> Result<...>blocks and using?. - For the two “f-blocker” sites that cannot easily change signatures, introduced
unwrap_or(safe_default)behavior withtracing::warn!to avoid panics.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/pricing/utils.rs |
Replaces sqrt / from_f64 / Normal::new unwraps with typed DecimalError paths; updates a panic-based test to assert an error instead. |
src/pricing/telegraph.rs |
Removes unwraps in state transition and simulation loop; adds warning+default on a conversion failure path. |
src/pricing/payoff.rs |
Removes to_f64().unwrap() in standard_payoff via unwrap_or_else + warn!; updates doctest snippets. |
src/pricing/monte_carlo.rs |
Converts payoff/rate conversions to error-returning paths instead of panicking; fixes Decimal::from_usize unwrap. |
src/pricing/compound.rs |
Replaces Positive::new(...).unwrap() with map_err + ?. |
src/pricing/cliquet.rs |
Guards partial_cmp sorting by explicitly rejecting NaN reset dates; replaces from_f64 unwraps with errors. |
src/pricing/binomial_model.rs |
Eliminates unwrap in initial payoff vector creation via collect::<Result<_,_>>()?; updates doctest. |
src/pricing/barrier.rs |
Converts from_f64 and sqrt unwraps into typed PricingError paths. |
src/pricing/asian.rs |
Removes Positive::new(...).unwrap() and replaces sqrt(t) unwrap usage with a typed error. |
src/pricing/american.rs |
Updates module/function doctests to use fallible Positive::new(...)? instead of unwrap. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
7 tasks
PR #350 Copilot review feedback (7 of 8 comments addressed; the 8th flags a pre-existing math bug in TelegraphProcess::next_state and is tracked separately in #351). Doc fixes (4 sites in src/pricing/utils.rs + 1 in monte_carlo.rs): - probability_keep_under_strike: rustdoc said `strike: Option<f64>` and "returns f64". Updated to match the actual signature (`Option<Positive>`, `Result<Decimal, DecimalError>`) and added an explicit # Errors section. - calculate_option_price: rustdoc said `u`/`d` were `f64` and the return was `f64`. Updated to `Decimal` and `Result<Decimal, DecimalError>` with a # Errors section. - calculate_discounted_payoff: rustdoc said it returned `f64`. Updated to `Result<Decimal, DecimalError>` with # Errors. - wiener_increment: rustdoc said it returned `f64` and had a # Panics section. Now `Result<Decimal, DecimalError>`, no panic. Replaced # Panics with a proper # Errors section. - price_option_monte_carlo (monte_carlo.rs): the # Errors section still said "Panics if option.payoff_at_price fails". The current implementation uses `unwrap_or(Decimal::ZERO)`, so the function cannot panic. Updated to describe the silent zero-payoff fallback and the new num_simulations Decimal-conversion error path. Code fixes (2): - src/pricing/utils.rs::simulate_returns sqrt error message changed from "non-finite time_step" to "invalid (negative or non-finite) time_step" — Decimal::sqrt() returns None for negative input too, not just non-finite. Same wording the reviewer suggested. - src/pricing/telegraph.rs::telegraph: replaced the lossy `Decimal::from_f64(no_steps as f64).ok_or_else(...)` with `Decimal::from_usize(no_steps).ok_or_else(...)` and added an explicit zero-guard returning PricingError::method_error early so we never divide by zero when computing `dt`. Matches the reviewer's suggestion verbatim. Build: cargo build --all-targets --all-features clean. Clippy: cargo clippy --all-targets --all-features --workspace -- -D warnings clean. Tests: cargo test --lib pricing:: 240 passed; 0 failed. Doctests: 203 passed; 0 failed. Follow-up #351 tracks the larger lambda_dt branch fix in TelegraphProcess::next_state (out of scope for #318).
Owner
Author
This was referenced Apr 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #318.
Summary
Remove every production
.unwrap()/.expect()fromsrc/pricing/(M1 — Panic-Free Core, 0.16.0). Total: 48 sites across 10 files → 0.Acceptance grep is clean:
Approach
Per-file site counts:
utils.rs?viaPricingError::method_error/DecimalErrortelegraph.rs?; F-blockernext_state -> i8resolved in-place withunwrap_or(0.0)+tracing::warn!payoff.rs# fn run() -> Result<...>; F-blockerstandard_payoff -> f64resolved in-placeamerican.rsbarrier.rsfrom_f64+ sqrt →ok_or_else+?asian.rsPositive::new→map_err/?monte_carlo.rsto_f64+from_usize→ok_or_else+?cliquet.rspartial_cmpguarded with NaN check, 2from_f64→?binomial_model.rsVec::collect::<Result<_, _>>()?+ 1 doctest wrapcompound.rsPositive::new(2.0)→map_err+?Both F-blockers (
telegraph::next_state -> i8andpayoff::standard_payoff -> f64) used the in-placeunwrap_or(safe_default)+tracing::warn!strategy because:next_state: probability is provably bounded[0, 1](just compared< probabilityafter samplingrandom::<f64>()).standard_payoff:(spot - strike).max(Decimal::ZERO)is the max of two finite Decimals, provably finite.Both fall under
rules/global_rules.md§Numerical Discipline (NaN/Inf guard at f64↔Decimal boundaries), matching the precedent in PR #348 forchains/utils.rs::adjust_volatility.panic!/unreachable!/unimplemented!/assert!are intentionally left in place — they belong to issue #292.Public API impact
None. All
pub fnsignatures preserved. Trait surfaces (Payoff,PayoffInfo,Profit) unchanged. No new error variants. No newFromimpls. README regen produces no diff.One test repair:
#[should_panic]testtest_probability_keep_under_strike_zero_volatility(utils.rs:647) becameassert!(... .is_err())since the panic is now a typedDecimalError— exactly the goal of this issue.Verification
cargo build --all-targets --all-features— clean.cargo clippy --all-targets --all-features --workspace -- -D warnings— clean.cargo fmt --all --check— clean.cargo test --lib pricing::— 240 passed; 0 failed.cargo test --lib --no-fail-fast— 3723 passed; 0 failed; 5 ignored.cargo test --doc— 203 passed; 0 failed.cargo build --release— clean.Commit map
refactor(pricing): panic-free src/pricing/* (48 sites, no API change)Test plan
cargo build --all-targets --all-featurescleancargo clippy --all-targets --all-features --workspace -- -D warningscleancargo fmt --all --checkcleancargo test --libcleancargo test --doccleancargo build --releasecleanunwrap_or+tracing::warn!strategy is acceptable for the two F-blockers (telegraph::next_state,payoff::standard_payoff)