From 1d32c3091855a442275ffa29a6556814975d58e9 Mon Sep 17 00:00:00 2001 From: Brad Smith Date: Wed, 10 Jun 2026 13:51:14 -0500 Subject: [PATCH] fix(api): use N(d2) for probability of profit, not N(d1) _calculate_probability_above() returned norm.cdf(d1) as the probability that the underlying finishes above a target. The risk-neutral probability P[S_T > K] is N(d2), not N(d1); N(d1) is the call delta and is always larger, so every probability-of-profit figure was overstated. Every POP value in the system flows through this method (directly and via _calculate_probability_below / _between), so the bias was system-wide. Compute d2 = d1 - sigma*sqrt(t) and return N(d2). For S=100, K=105, vol=0.25, 30d, r=0.05 this corrects 0.2784 -> 0.2549 (a 2.35 pt overstatement removed). Closes #8 Co-Authored-By: Claude Opus 4.8 --- src/api/enhanced_strategy_recommender.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/enhanced_strategy_recommender.py b/src/api/enhanced_strategy_recommender.py index 3c68d49..7a9091d 100644 --- a/src/api/enhanced_strategy_recommender.py +++ b/src/api/enhanced_strategy_recommender.py @@ -542,9 +542,12 @@ def _calculate_probability_above(self, current_price: float, target_price: float t = days / 365.0 d1 = (np.log(current_price / target_price) + (self.risk_free_rate + 0.5 * volatility**2) * t) / (volatility * np.sqrt(t)) + # Risk-neutral probability that S_T > target is N(d2), not N(d1). + # N(d1) is the call delta; using it overstates the probability. + d2 = d1 - volatility * np.sqrt(t) from scipy.stats import norm - return norm.cdf(d1) + return norm.cdf(d2) def _calculate_probability_below(self, current_price: float, target_price: float, volatility: float, days: int) -> float: