Skip to content

Commit d7d4d38

Browse files
committed
address review: dedup strategy trace! P&L and clarify Overflow doc
Two small correctness / docs fixes surfaced in the review on #371. - `strategies::short_strangle::calculate_profit_at` and `strategies::short_straddle::calculate_profit_at` were calling `pnl_at_expiration` three times per leg: twice inside the `trace!` argument list and once inside `d_sum`. Besides the wasted work (`pnl_at_expiration` builds several `Position` clones and re-runs premium and fee arithmetic), it meant the `trace!` snapshot and the returned total could differ if any leg had intermittent errors or side-effecting dependencies. Both implementations now compute `call_pnl` / `put_pnl` once, feed them into `d_sum` to get `total`, and use the three cached locals in the `trace!` line and the `Ok` return. - `DecimalError::Overflow` docs were reworded to match the actual behaviour: the operands are part of the `#[error]` display template and therefore visible in any formatted error string that reaches downstream modules via the `#[from]` cascade. The previous note claimed the operands were "never re-emitted through the public API surface", which is incorrect. The doc now explicitly points callers that need to redact operand values towards a custom `Display` / match-and-reformat path. No functional API change. `cargo test --lib` passes (3751 / 5 ignored / 0 regressions). `cargo clippy --lib --all-features` reports zero warnings. `make pre-push` is clean. Refs #335, PR #371
1 parent 6fd7294 commit d7d4d38

7 files changed

Lines changed: 56 additions & 72 deletions

File tree

src/error/decimal.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,14 @@ pub enum DecimalError {
139139
/// `"pricing::black_scholes::discount_strike"`) so failures can be
140140
/// traced back to their kernel without walking the call stack.
141141
///
142-
/// The operands are captured verbatim for post-mortem debugging; they
143-
/// are never re-emitted through the public API surface because
144-
/// `DecimalError` already reaches every domain module through the
145-
/// existing `#[from]` cascade.
142+
/// The operands are captured verbatim for post-mortem debugging and
143+
/// are included in the formatted error output (see the `#[error]`
144+
/// template below). This `DecimalError` variant is propagated across
145+
/// domain modules through the existing `#[from]` cascade, so the
146+
/// operand values will appear in any log or wrapper-error string
147+
/// produced downstream. Callers that need to redact them for
148+
/// production logging should match on the `Overflow` variant
149+
/// explicitly and reformat rather than rely on the default `Display`.
146150
#[error("Decimal {operation} overflow: {lhs} op {rhs}")]
147151
Overflow {
148152
/// Short static tag identifying the call-site.

src/greeks/numerical.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,7 @@ pub fn numerical_gamma(option: &Options) -> Result<Decimal, GreeksError> {
8282
// p_plus - 2*p + p_minus.
8383
// Build `2*p` via `d_mul` so an overflow on the doubled price does
8484
// not silently saturate before the checked `d_sub` / `d_add`.
85-
let two_p = d_mul(
86-
dec!(2.0),
87-
p.to_dec(),
88-
"greeks::numerical::gamma::two_p",
89-
)?;
85+
let two_p = d_mul(dec!(2.0), p.to_dec(), "greeks::numerical::gamma::two_p")?;
9086
let step = d_sub(p_plus.to_dec(), two_p, "greeks::numerical::gamma::step")?;
9187
let numer = d_add(step, p_minus.to_dec(), "greeks::numerical::gamma::numer")?;
9288
let h_squared = d_mul(H, H, "greeks::numerical::gamma::h_squared")?;

src/pricing/chooser.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -163,21 +163,13 @@ fn simple_chooser_price(option: &Options, choice_date_days: f64) -> Result<Decim
163163
dividend_discount_t,
164164
"pricing::chooser::price::leg_s_t_discounted",
165165
)?;
166-
let leg_s_t = d_mul(
167-
leg_s_t_discounted,
168-
n_d1,
169-
"pricing::chooser::price::leg_s_t",
170-
)?;
166+
let leg_s_t = d_mul(leg_s_t_discounted, n_d1, "pricing::chooser::price::leg_s_t")?;
171167
let leg_k_t_discounted = d_mul(
172168
k.to_dec(),
173169
discount_t,
174170
"pricing::chooser::price::leg_k_t_discounted",
175171
)?;
176-
let leg_k_t = d_mul(
177-
leg_k_t_discounted,
178-
n_d2,
179-
"pricing::chooser::price::leg_k_t",
180-
)?;
172+
let leg_k_t = d_mul(leg_k_t_discounted, n_d2, "pricing::chooser::price::leg_k_t")?;
181173
let leg_k_choice_discounted = d_mul(
182174
k.to_dec(),
183175
discount_choice,

src/pricing/compound.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,15 @@ fn price_compound(
340340
// overflow on either monetary product surfaces a tagged
341341
// `DecimalError::Overflow` instead of saturating silently before
342342
// the final `d_add` / `d_sub`.
343-
let build_leg =
344-
|base: Decimal, discount: Decimal, weight: Decimal, op_base: &'static str,
345-
op_leg: &'static str|
346-
-> Result<Decimal, PricingError> {
347-
let discounted = d_mul(base, discount, op_base)?;
348-
Ok(d_mul(discounted, weight, op_leg)?)
349-
};
343+
let build_leg = |base: Decimal,
344+
discount: Decimal,
345+
weight: Decimal,
346+
op_base: &'static str,
347+
op_leg: &'static str|
348+
-> Result<Decimal, PricingError> {
349+
let discounted = d_mul(base, discount, op_base)?;
350+
Ok(d_mul(discounted, weight, op_leg)?)
351+
};
350352

351353
let price = if is_compound_call && is_underlying_call {
352354
// Call-on-Call

src/strategies/short_straddle.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -843,23 +843,25 @@ impl Optimizable for ShortStraddle {
843843
impl Profit for ShortStraddle {
844844
fn calculate_profit_at(&self, price: &Positive) -> Result<Decimal, PricingError> {
845845
let price = Some(price);
846+
// Compute each leg's P&L exactly once and reuse for both the
847+
// `trace!` line and the checked `d_sum`, so the evaluator
848+
// never observes two different snapshots of the same leg.
849+
let call_pnl = self.short_call.pnl_at_expiration(&price)?;
850+
let put_pnl = self.short_put.pnl_at_expiration(&price)?;
851+
let total = d_sum(
852+
&[call_pnl, put_pnl],
853+
"strategies::short_straddle::profit_at",
854+
)?;
846855
trace!(
847856
"Price: {:?} Strike: {} Call: {:.2} Strike: {} Put: {:.2} Profit: {:.2}",
848857
price,
849858
self.short_call.option.strike_price,
850-
self.short_call.pnl_at_expiration(&price)?,
859+
call_pnl,
851860
self.short_put.option.strike_price,
852-
self.short_put.pnl_at_expiration(&price)?,
853-
self.short_call.pnl_at_expiration(&price)?
854-
+ self.short_put.pnl_at_expiration(&price)?
861+
put_pnl,
862+
total
855863
);
856-
Ok(d_sum(
857-
&[
858-
self.short_call.pnl_at_expiration(&price)?,
859-
self.short_put.pnl_at_expiration(&price)?,
860-
],
861-
"strategies::short_straddle::profit_at",
862-
)?)
864+
Ok(total)
863865
}
864866
}
865867

src/strategies/short_strangle.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,22 +1133,25 @@ impl Optimizable for ShortStrangle {
11331133
impl Profit for ShortStrangle {
11341134
fn calculate_profit_at(&self, price: &Positive) -> Result<Decimal, PricingError> {
11351135
let price = &Some(price);
1136+
// Compute each leg's P&L exactly once and reuse for both the
1137+
// `trace!` line and the checked `d_sum`, so the evaluator
1138+
// never observes two different snapshots of the same leg.
1139+
let call_pnl = self.short_call.pnl_at_expiration(price)?;
1140+
let put_pnl = self.short_put.pnl_at_expiration(price)?;
1141+
let total = d_sum(
1142+
&[call_pnl, put_pnl],
1143+
"strategies::short_strangle::profit_at",
1144+
)?;
11361145
trace!(
11371146
"Price: {:?} Strike: {} Call: {:.2} Strike: {} Put: {:.2} Profit: {:.2}",
11381147
price,
11391148
self.one_option().strike_price,
1140-
self.short_call.pnl_at_expiration(price)?,
1149+
call_pnl,
11411150
self.short_put.option.strike_price,
1142-
self.short_put.pnl_at_expiration(price)?,
1143-
self.short_call.pnl_at_expiration(price)? + self.short_put.pnl_at_expiration(price)?
1144-
);
1145-
Ok(d_sum(
1146-
&[
1147-
self.short_call.pnl_at_expiration(price)?,
1148-
self.short_put.pnl_at_expiration(price)?,
1149-
],
1150-
"strategies::short_strangle::profit_at",
1151-
)?)
1151+
put_pnl,
1152+
total
1153+
);
1154+
Ok(total)
11521155
}
11531156
}
11541157

src/volatility/utils.rs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,8 @@ pub fn ewma_volatility(
165165
// innovation term.
166166
let persistent = d_mul(lambda, variance, "volatility::ewma::persistent")?;
167167
let innovation_weight = d_sub(Decimal::ONE, lambda, "volatility::ewma::innovation_weight")?;
168-
let return_sq = d_mul(
169-
return_value,
170-
return_value,
171-
"volatility::ewma::return_sq",
172-
)?;
173-
let innovation = d_mul(
174-
innovation_weight,
175-
return_sq,
176-
"volatility::ewma::innovation",
177-
)?;
168+
let return_sq = d_mul(return_value, return_value, "volatility::ewma::return_sq")?;
169+
let innovation = d_mul(innovation_weight, return_sq, "volatility::ewma::innovation")?;
178170
variance = d_add(persistent, innovation, "volatility::ewma::variance")?;
179171
let std_dev = variance
180172
.sqrt()
@@ -353,15 +345,12 @@ pub fn garch_volatility(
353345
// the rejection (negative variance) onto a readable
354346
// `VolatilityError::NumericalFailure` instead of silently
355347
// clamping to zero.
356-
let to_positive_variance = |value: Decimal,
357-
context: &'static str|
358-
-> Result<Positive, VolatilityError> {
359-
Positive::new_decimal(value).map_err(|_| VolatilityError::NumericalFailure {
360-
reason: format!(
361-
"garch_volatility: negative variance at {context} ({value})"
362-
),
363-
})
364-
};
348+
let to_positive_variance =
349+
|value: Decimal, context: &'static str| -> Result<Positive, VolatilityError> {
350+
Positive::new_decimal(value).map_err(|_| VolatilityError::NumericalFailure {
351+
reason: format!("garch_volatility: negative variance at {context} ({value})"),
352+
})
353+
};
365354

366355
// Seed the recursion with `r_0^2` via `d_mul` so a saturating
367356
// squaring cannot feed a silently capped value into the GARCH
@@ -376,11 +365,7 @@ pub fn garch_volatility(
376365
for &return_value in &returns[1..] {
377366
// GARCH(1, 1) variance recursion:
378367
// v' = ω + α · r² + β · v_prev.
379-
let return_sq = d_mul(
380-
return_value,
381-
return_value,
382-
"volatility::garch::return_sq",
383-
)?;
368+
let return_sq = d_mul(return_value, return_value, "volatility::garch::return_sq")?;
384369
let alpha_r2 = d_mul(alpha, return_sq, "volatility::garch::alpha_r2")?;
385370
let beta_v = d_mul(beta, variance.to_dec(), "volatility::garch::beta_v")?;
386371
let persistent = d_add(omega, alpha_r2, "volatility::garch::omega_plus_alpha")?;

0 commit comments

Comments
 (0)