Historical vs implied volatility, IV rank/percentile
Chapter 120 computed IV — the market's *forward-looking* volatility estimate embedded in option prices. This chapter contrasts it with *realized* (historical) volatility, and builds the IV Rank/Percentile metrics that tell you whether current IV is cheap or expensive relative to its own recent history.
Historical (realized) volatility
def historical_volatility(price_series: pd.Series, period: int = 20, annualization_factor: int = 252) -> pd.Series:
log_returns = np.log(price_series / price_series.shift(1))
return log_returns.rolling(period).std() * math.sqrt(annualization_factor)
annualization_factor=252 assumes daily data (approx. trading days per year); for intraday data, adjust to the number of bars per year at that timeframe.
IV vs HV — the core comparison
def iv_hv_spread(current_iv: float, hv_series: pd.Series) -> float:
current_hv = hv_series.iloc[-1]
return current_iv - current_hv
IV consistently trading above HV ("volatility risk premium") is the normal state in most options markets, including Indian index options — option sellers are compensated for bearing the tail risk that realized volatility occasionally spikes above what was priced in. A persistently *negative* spread (IV below HV) is less common and can indicate options are unusually cheap relative to recent actual price behavior.
IV Rank and IV Percentile — the standard normalized metrics
def iv_rank(current_iv: float, iv_history: pd.Series) -> float:
"""Where current IV sits between the historical min and max, as a percentage."""
iv_min, iv_max = iv_history.min(), iv_history.max()
return (current_iv - iv_min) / (iv_max - iv_min) * 100 if iv_max > iv_min else 50
def iv_percentile(current_iv: float, iv_history: pd.Series) -> float:
"""What percentage of historical IV readings were BELOW the current level."""
return (iv_history < current_iv).mean() * 100
Why these two metrics can disagree, and why that matters
IV Rank is sensitive to extreme outliers (a single historical volatility spike stretches the min-max range, making current IV look artificially "low" even if it's actually elevated relative to most of history). IV Percentile is more robust to outliers since it's based on the full distribution, not just the range endpoints. Prefer IV Percentile as the more reliable metric when historical data includes any extreme event periods (e.g. a dataset spanning the 2020 COVID volatility spike).
def build_iv_history(nfo_df, kite, underlying: str, lookback_days: int = 252) -> pd.Series:
"""Computes ATM IV for the near-month contract, once per day, over the lookback period.
Requires historical option price data — a real data engineering task,
since Kite's historical_data() doesn't retroactively give you the option chain
as it existed on past dates unless you've been recording it yourself daily."""
... # implementation depends on your own historical option-price recording pipeline
Note the caveat in that docstring — building a genuine IV history requires you to have been recording option prices going forward; you generally cannot reconstruct historical ATM IV for arbitrary past dates from the broker API alone. Start recording daily ATM IV snapshots now if you want this metric to be meaningful in a few months.
Practical use: informing whether to buy or sell options
A common (not infallible) heuristic: prefer strategies that are net option-selling (credit spreads, short straddles/strangles) when IV Percentile is high (options relatively expensive, decay/premium favors sellers), and prefer net option-buying strategies when IV Percentile is low (options relatively cheap). This is a starting heuristic to backtest (chapter 82), not a rule to apply blindly — validate it against your own strategy and instrument rather than assuming it holds universally.