Skip to content

M1: Eliminate .unwrap()/.expect() from src/pricing/ (#318)#350

Merged
joaquinbejar merged 2 commits intomainfrom
issue-318-pricing-panic-free
Apr 17, 2026
Merged

M1: Eliminate .unwrap()/.expect() from src/pricing/ (#318)#350
joaquinbejar merged 2 commits intomainfrom
issue-318-pricing-panic-free

Conversation

@joaquinbejar
Copy link
Copy Markdown
Owner

Closes #318.

Summary

Remove every production .unwrap() / .expect() from src/pricing/ (M1 — Panic-Free Core, 0.16.0). Total: 48 sites across 10 files → 0.

Acceptance grep is clean:

for f in $(find src/pricing -name '*.rs'); do
  awk '/#\[cfg\(test\)\]/{exit} {print}' "$f" | grep -nE '\.unwrap\(\)|\.expect\('
done
# (no output)

Approach

Per-file site counts:

File Sites Pattern
utils.rs 11 sqrt + from_f64 + Normal::new + chain unwraps → ? via PricingError::method_error / DecimalError
telegraph.rs 9 mostly ?; F-blocker next_state -> i8 resolved in-place with unwrap_or(0.0) + tracing::warn!
payoff.rs 6 4 doctests rewrapped in # fn run() -> Result<...>; F-blocker standard_payoff -> f64 resolved in-place
american.rs 5 doctests rewrapped
barrier.rs 4 from_f64 + sqrt → ok_or_else + ?
asian.rs 4 sqrt + Positive::newmap_err / ?
monte_carlo.rs 3 to_f64 + from_usizeok_or_else + ?
cliquet.rs 3 partial_cmp guarded with NaN check, 2 from_f64?
binomial_model.rs 2 Vec::collect::<Result<_, _>>()? + 1 doctest wrap
compound.rs 1 Positive::new(2.0)map_err + ?

Both F-blockers (telegraph::next_state -> i8 and payoff::standard_payoff -> f64) used the in-place unwrap_or(safe_default) + tracing::warn! strategy because:

  1. next_state: probability is provably bounded [0, 1] (just compared < probability after sampling random::<f64>()).
  2. 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 for chains/utils.rs::adjust_volatility.

panic! / unreachable! / unimplemented! / assert! are intentionally left in place — they belong to issue #292.

Public API impact

None. All pub fn signatures preserved. Trait surfaces (Payoff, PayoffInfo, Profit) unchanged. No new error variants. No new From impls. README regen produces no diff.

One test repair: #[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.

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

  1. refactor(pricing): panic-free src/pricing/* (48 sites, no API change)

Test plan

  • 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 clean
  • cargo test --doc clean
  • cargo build --release clean
  • Reviewer confirms in-place unwrap_or + tracing::warn! strategy is acceptable for the two F-blockers (telegraph::next_state, payoff::standard_payoff)

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
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 61.31387% with 53 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pricing/telegraph.rs 48.57% 18 Missing ⚠️
src/pricing/monte_carlo.rs 38.88% 11 Missing ⚠️
src/pricing/barrier.rs 53.33% 7 Missing ⚠️
src/pricing/utils.rs 80.00% 7 Missing ⚠️
src/pricing/cliquet.rs 44.44% 5 Missing ⚠️
src/pricing/payoff.rs 60.00% 4 Missing ⚠️
src/pricing/asian.rs 90.90% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/pricing/american.rs 90.32% <ø> (ø)
src/pricing/binomial_model.rs 83.51% <100.00%> (ø)
src/pricing/compound.rs 77.16% <100.00%> (+0.21%) ⬆️
src/pricing/asian.rs 81.48% <90.90%> (-0.08%) ⬇️
src/pricing/payoff.rs 63.33% <60.00%> (-4.85%) ⬇️
src/pricing/cliquet.rs 82.43% <44.44%> (-5.81%) ⬇️
src/pricing/barrier.rs 72.35% <53.33%> (-3.96%) ⬇️
src/pricing/utils.rs 88.42% <80.00%> (-6.32%) ⬇️
src/pricing/monte_carlo.rs 73.17% <38.88%> (-26.83%) ⬇️
src/pricing/telegraph.rs 80.50% <48.57%> (-11.97%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 typed PricingError/DecimalError propagation.
  • 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 with tracing::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.

Comment thread src/pricing/utils.rs
Comment thread src/pricing/utils.rs Outdated
Comment thread src/pricing/telegraph.rs
Comment thread src/pricing/telegraph.rs Outdated
Comment thread src/pricing/monte_carlo.rs
Comment thread src/pricing/utils.rs
Comment thread src/pricing/utils.rs
Comment thread src/pricing/utils.rs
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).
@joaquinbejar
Copy link
Copy Markdown
Owner Author

Thanks for the review. Seven of the eight comments are addressed in c162f41 (5 docstring fixes plus the sqrt error wording and the telegraph zero-guard). The remaining lambda_dt branch bug is a real pre-existing logic error and is tracked separately in #351.

@joaquinbejar joaquinbejar merged commit d8af5c4 into main Apr 17, 2026
11 checks passed
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.

Eliminate .unwrap()/.expect() in src/pricing/

2 participants