|
16 | 16 | VendorSnapshot, |
17 | 17 | resolve_market, |
18 | 18 | ) |
19 | | -from oipd.pipelines.vol_curve import fit_vol_curve_internal |
| 19 | +from oipd.pipelines.vol_curve import fit_vol_curve_internal, compute_fitted_smile |
20 | 20 | from oipd.pipelines.distribution import derive_distribution_from_curve |
21 | 21 | from oipd.pipelines.vol_surface import fit_surface |
22 | 22 | from oipd.pipelines.vol_surface.models import FittedSurface |
23 | 23 |
|
| 24 | +from oipd.presentation.iv_plotting import plot_iv_smile, ReferenceAnnotation |
| 25 | + |
24 | 26 |
|
25 | 27 | class VolCurve: |
26 | 28 | """Single-expiry implied-volatility estimator with sklearn-style API. |
@@ -155,6 +157,83 @@ def at_money_vol(self) -> float: |
155 | 157 | raise ValueError("Call fit before accessing ATM volatility") |
156 | 158 | return getattr(self._vol_curve, "at_money_vol", np.nan) |
157 | 159 |
|
| 160 | + def iv_smile( |
| 161 | + self, |
| 162 | + domain: Optional[tuple[float, float]] = None, |
| 163 | + points: int = 200, |
| 164 | + include_observed: bool = True, |
| 165 | + ) -> pd.DataFrame: |
| 166 | + """Return the implied-volatility smile with fitted and market-observed values. |
| 167 | +
|
| 168 | + Args: |
| 169 | + domain: Optional (min, max) strike range. If None, inferred from observed data. |
| 170 | + points: Number of points in the grid. |
| 171 | + include_observed: Whether to include market-observed IVs. If False, only |
| 172 | + ``strike`` and ``fitted_iv`` columns are returned. |
| 173 | +
|
| 174 | + Returns: |
| 175 | + DataFrame containing: |
| 176 | + |
| 177 | + - ``strike``: Strike levels. |
| 178 | + - ``fitted_iv``: Fitted implied volatility from the calibrated model. |
| 179 | + - ``market_iv``: *(if include_observed=True)* Mid-price implied volatility |
| 180 | + computed by inverting the mid option price using the same pricing model |
| 181 | + (Black-76 or Black-Scholes), risk-free rate, dividend assumptions, and |
| 182 | + time-to-expiry as specified during ``.fit()``. |
| 183 | + - ``market_bid_iv``: *(if include_observed=True)* Bid-price implied volatility |
| 184 | + computed using the same methodology. |
| 185 | + - ``market_ask_iv``: *(if include_observed=True)* Ask-price implied volatility |
| 186 | + computed using the same methodology. |
| 187 | + - ``market_last_iv``: *(if include_observed=True)* Last-price implied volatility |
| 188 | + computed using the same methodology (only included if bid/ask are unavailable). |
| 189 | +
|
| 190 | + Note: |
| 191 | + The market IVs are **not** raw quotes from exchanges—they are computed by |
| 192 | + the library by solving the inverse problem: "What σ makes the model price |
| 193 | + match the observed option price?" This ensures consistency with the fitted |
| 194 | + curve, which uses the same pricing assumptions. |
| 195 | + """ |
| 196 | + return compute_fitted_smile( |
| 197 | + vol_curve=self, |
| 198 | + metadata=self._metadata, |
| 199 | + domain=domain, |
| 200 | + points=points, |
| 201 | + include_observed=include_observed, |
| 202 | + ) |
| 203 | + |
| 204 | + def plot(self, **kwargs) -> Any: |
| 205 | + """Plot the fitted implied volatility smile. |
| 206 | + |
| 207 | + Args: |
| 208 | + **kwargs: Arguments forwarded to ``oipd.presentation.iv_plotting.plot_iv_smile``. |
| 209 | + |
| 210 | + Returns: |
| 211 | + matplotlib.figure.Figure: The plot figure. |
| 212 | + """ |
| 213 | + smile_df = self.iv_smile() |
| 214 | + |
| 215 | + # Map our column names to what plot_iv_smile expects |
| 216 | + plot_df = smile_df.rename(columns={ |
| 217 | + "market_bid_iv": "bid_iv", |
| 218 | + "market_ask_iv": "ask_iv", |
| 219 | + "market_last_iv": "last_iv" |
| 220 | + }) |
| 221 | + |
| 222 | + # Extract reference price (forward) for log-moneyness plotting |
| 223 | + reference = None |
| 224 | + forward_price = self._metadata.get("forward_price") |
| 225 | + if forward_price is not None: |
| 226 | + reference = ReferenceAnnotation( |
| 227 | + value=float(forward_price), |
| 228 | + label=f"Forward: {float(forward_price):.2f}" |
| 229 | + ) |
| 230 | + |
| 231 | + # If no reference price is available, default to strike axis to avoid errors |
| 232 | + if reference is None and "axis_mode" not in kwargs: |
| 233 | + kwargs["axis_mode"] = "strike" |
| 234 | + |
| 235 | + return plot_iv_smile(plot_df, reference=reference, **kwargs) |
| 236 | + |
158 | 237 | @property |
159 | 238 | def params(self) -> dict[str, Any]: |
160 | 239 | """Return fitted parameter dictionary from the underlying curve. |
|
0 commit comments