A Black–Scholes Formula Error That Distorts Implied Volatility
Summary
The document presents an attempt to infer implied volatility by minimizing the difference between observed option prices and Black–Scholes prices. The calculation uses spot, strike, time to expiry, interest rates, and dividend yield, with rates interpolated from a zero curve. The author compares model-derived volatility with values shown by a market data source and observes especially large discrepancies for options far from the money.
The accepted response identifies the main cause: a misplaced time factor in the definition of d1. The correct denominator scales volatility by the square root of time; multiplying by time instead can make the pricing calculation materially wrong and drive the optimizer toward its lower bound. After correcting the formula, the author reports a more plausible smile but still sees a jump near the money. The document notes that omitted dividends, differences in data or model conventions, and using out-of-the-money puts and calls may affect the comparison. It is a troubleshooting example, not a full treatment of robust implied-volatility solving or market data validation.
Key ideas
- Implied volatility can be estimated by finding the volatility that makes a model price match the observed option price.
- A misplaced time factor in d1 can severely distort Black–Scholes prices and the resulting volatility estimate.
- Comparing model outputs with quoted volatility can help expose formula or input errors.
- Dividend assumptions and differences in data conventions can still create discrepancies after the formula is corrected.
- Using out-of-the-money puts on one side of a smile and calls on the other can contribute to a discontinuity.
Tags
Full text
# finding Black Scholes implied volatility
# finding Black Scholes implied volatility
I am trying to imply BS volatilities using Yahoo Finance! API (MSFT options) and Reuters zero curves. I am ignoring dividends for the moment. Note that I've also cleant the options' data (e.g., I make sure that last trade day of the option was the day on which I am valuing, and to get a nice smile I've only kept OTM calls and OTM puts under the assumption that OTM options are chea
```
def f_call(vol, S, r, K, dt, q):
d1 = 1 / vol * np.sqrt(dt) * (np.log(S/K) + (r - q + vol**2 / 2) * dt)
d2 = d1 - vol * np.sqrt(dt)
return np.exp(-q*dt) * S * stats.norm.cdf(d1) - np.exp(-r*dt) * K * stats.norm.cdf(d2)
def f_put(vol, S, r, K, dt, q):
d1 = 1 / vol * np.sqrt(dt) * (np.log(S/K) + (r - q + vol**2 / 2) * dt)
d2 = d1 - vol * np.sqrt(dt)
return stats.norm.cdf(-d2) * K * np.exp(-r*dt) - stats.norm.cdf(-d1) * S * np.exp(-q*dt)
vol_guess = self.vol_yh[i]
print(i)
params = {
'S' : self.S,
'K' : self.K[i],
'dt' : self.dt[i],
'r' : self.r[i],
'q' : self.q[i],
}
option_type = self.type[i]
if option_type == 'C':
f = self.f_call
else:
f = self.f_put
bounds = optimize.Bounds(lb=0, ub=100)
e = lambda s: np.sqrt(np.mean((f(s, **params) - market_price[i]) ** 2))
# e = lambda s: f(s, **params) - market_price[i]
res = optimize.minimize(fun=e, x0=(vol_guess,), method='SLSQP', bounds=bounds)
res = res.x[0]
# res = optimize.newton(func=e, x0=vol_guess)
# print(res)
print(f'Yahoo volatility: {self.vol_yh[i]}')
print(f'Model volatility: {res}')
self.vol[i] = res
```
In this example, `params = {'S': 268.75, 'K': 80.0, 'dt': 0.28055555555555556, 'r': 0.015503659068441186, 'q': 0.0}`, where `dt` is 'ACT/365' and 'r' is found by interpolating linearly on `dt`.
For reference, I am posting also a snapshot of the zero rates ('deltaTime' is also ACT/365). These come from RIC ticker USDSROISZ=, which are zero rates boostrapped from USDSROIS (SOFR OIS par rates), which I think is good enough to get sensible results. As you can see the rate for my option is between 5 and 6.
```
deltaTime zeroRates
0 0.002778 0.007940
1 0.005556 0.008077
2 0.019444 0.009127
3 0.083333 0.011614
4 0.169444 0.013341
5 0.255556 0.015036
6 0.508333 0.019158
7 0.758333 0.022491
8 1.013889 0.024985
9 1.269444 0.026642
10 1.522222 0.027639
11 1.775000 0.028254
12 2.030556 0.028567
13 2.286111 0.028716
```
However, there is something terribly wrong going on since the results I am getting are way different than the quoted Yahoo Finance! volatility.
```
Yahoo volatility: 0.7812521875
Model volatility: 8.279821273049492e-12
```
Interestingly, if I move toward the money, I get sensible results:
```
params = {'S': 268.75, 'K': 250.0, 'dt': 0.28055555555555556, 'r': 0.015503659068441186, 'q': 0.0}
Yahoo volatility: 0.3357610369873046
Model volatility: 0.3448098337803358
```
And finally, if I run the whole continuum of options for that particular expiry, I get the following volatility smile plots:
I understand that I can't match it perfectly, also dividends are playing a part (which I need to add), Yahoo could also have a different model, however I don't understand why the deep OTM and ITM options are SO different. Note that I've also cleant the options' data (e.g., I make sure that last trade day of the option was the day on which I am valuing, and to get a nice smile I've only kept OTM calls and OTM puts under the assumption that OTM options are cheaper and therefore more liquid). But, in my opinion, none of this can explain the huge differences in the deep moneyness. What is happening is that the minimization hits the lower bound so it's very close to 0, but then again, why would the minimization want to move to negative volatility?
EDIT:
Fixed the typo in `d1`, and I now get a much more sensible smile, however I have a jump ATM. It might be because I use puts on the right and calls on the left, the OTM calls are actually overpriced because dividends are not taken into account I believe.
## Answer by BEQuant (score 4, accepted)
https://quant.stackexchange.com/a/71196
I think you get it wrong your definition of $d_1$. Shouldn't it be $d_1 = 1 / (\text{vol} * \text{dt}) * (\log(S/K) + (r - q + \text{vol}^2 / 2) * dt)$ and not $d_1 = 1 / \text{vol} * \text{dt} * (\log(S/K) + (r - q + \text{vol}^2 / 2) * dt)$ ?Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.