From dcb08125c9bae18cd6c42e5beace04abc5622da7 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 22 May 2026 22:49:32 +0100 Subject: [PATCH] Rename implied vols to iv --- app/api/heston.py | 6 +-- app/scripts/heston_divfm_fit.py | 4 +- docs/examples/pricing_method_comparison.py | 6 +-- frontend/src/heston-vol-surface.md | 14 +++--- frontend/src/volatility-surface.md | 10 ++--- notebooks/heston_divfm_fit.py | 4 +- quantflow/ai/tools/crypto.py | 2 +- quantflow/options/calibration/base.py | 18 ++++---- quantflow/options/calibration/bns.py | 4 +- quantflow/options/calibration/heston.py | 8 ++-- quantflow/options/divfm/pricer.py | 6 +-- quantflow/options/divfm/trainer.py | 4 +- quantflow/options/pricer.py | 6 +-- quantflow/options/surface.py | 48 +++++++++------------ quantflow/options/svi.py | 2 +- quantflow/utils/plot.py | 6 +-- quantflow_tests/test_ai.py | 2 +- quantflow_tests/test_disable_outliers.py | 6 +-- quantflow_tests/test_divfm.py | 32 +++++++------- quantflow_tests/test_non_inverse_surface.py | 2 +- quantflow_tests/test_options.py | 8 ++-- quantflow_tests/test_svi.py | 30 ++++++------- 22 files changed, 110 insertions(+), 118 deletions(-) diff --git a/app/api/heston.py b/app/api/heston.py index 62bb08c..7d70914 100644 --- a/app/api/heston.py +++ b/app/api/heston.py @@ -13,7 +13,7 @@ class VolSurfaceGridResponse(BaseModel): moneyness: list[float] = Field(description="Moneyness grid values") ttm: list[float] = Field(description="Time to maturity grid values") - implied_vol: list[list[float]] = Field( + iv: list[list[float]] = Field( description="Implied vol grid (rows=ttm, cols=moneyness)" ) @@ -68,7 +68,7 @@ async def heston_vol_surface( implied = np.zeros((len(ttm_arr), len(moneyness_arr))) for i, t in enumerate(ttm_arr): maturity = pricer.maturity(float(t)) - vols = maturity.prices(moneyness_arr * np.sqrt(t))["implied_vol"].values + vols = maturity.prices(moneyness_arr * np.sqrt(t))["iv"].values # replace NaN/Inf/negative with 0 vols = np.where(np.isfinite(vols) & (vols > 0), vols, 0.0) implied[i, :] = vols @@ -76,5 +76,5 @@ async def heston_vol_surface( return VolSurfaceGridResponse( moneyness=[float(m) for m in moneyness_arr], ttm=[float(t) for t in ttm_arr], - implied_vol=[[float(v) for v in row] for row in implied], + iv=[[float(v) for v in row] for row in implied], ) diff --git a/app/scripts/heston_divfm_fit.py b/app/scripts/heston_divfm_fit.py index 12a830e..8b08fd7 100644 --- a/app/scripts/heston_divfm_fit.py +++ b/app/scripts/heston_divfm_fit.py @@ -85,7 +85,7 @@ def _sample_day(rng: np.random.Generator, pricer: OptionPricer) -> DayData | Non np.float32 ) moneyness = m_ttm * np.sqrt(ttm) - ivs = np.interp(moneyness, mat.moneyness, mat.implied_vols) + ivs = np.interp(moneyness, mat.moneyness, mat.ivs) # drop any degenerate points (NaN / non-positive IV) valid = np.isfinite(ivs) & (ivs > 0) @@ -102,7 +102,7 @@ def _sample_day(rng: np.random.Generator, pricer: OptionPricer) -> DayData | Non return DayData( moneyness_ttm=np.concatenate(m_list), ttm=np.concatenate(t_list), - implied_vols=np.concatenate(iv_list), + ivs=np.concatenate(iv_list), ) diff --git a/docs/examples/pricing_method_comparison.py b/docs/examples/pricing_method_comparison.py index a8b8df3..74c0fe0 100644 --- a/docs/examples/pricing_method_comparison.py +++ b/docs/examples/pricing_method_comparison.py @@ -59,7 +59,7 @@ class PricingMethodComparison(BaseModel): description="Chart properties for each pricing method", ) - def _implied_vols( + def _ivs( self, r: OptionPricingResult, log_strikes: np.ndarray, ttm: float ) -> np.ndarray: call = np.asarray(r.call_price(log_strikes)) @@ -91,7 +91,7 @@ def run_ttm(self) -> None: self.ref_n + 1, max_log_strike=max_log_strike ) ref = ms.call_option(self.ref_n, max_moneyness=self.max_moneyness) - iv_ref = self._implied_vols(ref, log_strikes, ttm) + iv_ref = self._ivs(ref, log_strikes, ttm) moneyness_ref = log_strikes / np.sqrt(ttm) ttm_label = f"TTM={ttm}" slug = ttm_label.lower().replace("=", "").replace(".", "_") @@ -136,7 +136,7 @@ def run_ttm(self) -> None: fig.add_trace( go.Scatter( x=moneyness_ref, - y=self._implied_vols(r, log_strikes, ttm), + y=self._ivs(r, log_strikes, ttm), name=method.value, mode="lines", line=dict(color=props.color, dash=props.dash), diff --git a/frontend/src/heston-vol-surface.md b/frontend/src/heston-vol-surface.md index 7c3b878..d7fd0e4 100644 --- a/frontend/src/heston-vol-surface.md +++ b/frontend/src/heston-vol-surface.md @@ -59,8 +59,8 @@ const data = await fetchJson(`/.api/heston-vol-surface?${params}`); ```js const flat = data.ttm.flatMap((t, i) => - data.moneyness.map((m, j) => ({ttm: t.toFixed(2), moneyness: m, implied_vol: data.implied_vol[i][j]})) -).filter(d => d.implied_vol > 0); + data.moneyness.map((m, j) => ({ttm: t.toFixed(2), moneyness: m, iv: data.iv[i][j]})) +).filter(d => d.iv > 0); display(Plot.plot({ width: 800, @@ -72,7 +72,7 @@ display(Plot.plot({ y: {label: "Implied Volatility", percent: true}, color: {type: "ordinal", scheme: "turbo", legend: true, label: "TTM"}, marks: [ - Plot.line(flat, {x: "moneyness", y: "implied_vol", stroke: "ttm", strokeWidth: 1.5, tip: true}), + Plot.line(flat, {x: "moneyness", y: "iv", stroke: "ttm", strokeWidth: 1.5, tip: true}), Plot.ruleX([0], {stroke: "var(--theme-foreground-muted)", strokeDasharray: "4,4"}), ] })); @@ -83,8 +83,8 @@ display(Plot.plot({ ```js const atmByTtm = data.ttm.map((t, i) => { const midIdx = Math.floor(data.moneyness.length / 2); - return {ttm: t, implied_vol: data.implied_vol[i][midIdx]}; -}).filter(d => d.implied_vol > 0); + return {ttm: t, iv: data.iv[i][midIdx]}; +}).filter(d => d.iv > 0); display(Plot.plot({ width: 800, @@ -95,8 +95,8 @@ display(Plot.plot({ x: {label: "Time to Maturity"}, y: {label: "ATM Implied Volatility", percent: true}, marks: [ - Plot.line(atmByTtm, {x: "ttm", y: "implied_vol", stroke: "var(--theme-foreground-focus)", strokeWidth: 2}), - Plot.dot(atmByTtm, {x: "ttm", y: "implied_vol", fill: "var(--theme-foreground-focus)", r: 4, tip: true}), + Plot.line(atmByTtm, {x: "ttm", y: "iv", stroke: "var(--theme-foreground-focus)", strokeWidth: 2}), + Plot.dot(atmByTtm, {x: "ttm", y: "iv", fill: "var(--theme-foreground-focus)", r: 4, tip: true}), ] })); ``` diff --git a/frontend/src/volatility-surface.md b/frontend/src/volatility-surface.md index f3259fd..7285bef 100644 --- a/frontend/src/volatility-surface.md +++ b/frontend/src/volatility-surface.md @@ -41,7 +41,7 @@ const enriched = options.map(d => ({ log_strike: parseFloat(d.log_strike), moneyness: parseFloat(d.moneyness), ttm: parseFloat(d.ttm), - implied_vol: parseFloat(d.implied_vol), + iv: parseFloat(d.iv), price_bp: parseFloat(d.price_bp), open_interest: parseFloat(d.open_interest), volume: parseFloat(d.volume), @@ -123,7 +123,7 @@ display(Plot.plot({ marks: [ Plot.dot(smileData, { x: xAxis, - y: "implied_vol", + y: "iv", fill: "maturity", r: 3, opacity: 0.8, @@ -142,7 +142,7 @@ display(Plot.plot({ const atmByMaturity = maturities.map(m => { const slice = enriched.filter(d => d.maturity === m); const atm = slice.reduce((best, d) => Math.abs(d.moneyness) < Math.abs(best.moneyness) ? d : best); - return {maturity: m, implied_vol: atm.implied_vol}; + return {maturity: m, iv: atm.iv}; }); display(Plot.plot({ @@ -154,10 +154,10 @@ display(Plot.plot({ x: {label: "Maturity", type: "point"}, y: {label: "ATM Implied Volatility", percent: true}, marks: [ - Plot.line(atmByMaturity, {x: "maturity", y: "implied_vol", stroke: "var(--theme-foreground-focus)", strokeWidth: 2}), + Plot.line(atmByMaturity, {x: "maturity", y: "iv", stroke: "var(--theme-foreground-focus)", strokeWidth: 2}), Plot.dot(atmByMaturity, { x: "maturity", - y: "implied_vol", + y: "iv", fill: "var(--theme-foreground-focus)", r: 5, tip: true diff --git a/notebooks/heston_divfm_fit.py b/notebooks/heston_divfm_fit.py index 74e442d..61d6f3c 100644 --- a/notebooks/heston_divfm_fit.py +++ b/notebooks/heston_divfm_fit.py @@ -96,7 +96,7 @@ def _sample_day(rng: np.random.Generator, pricer: OptionPricer) -> DayData | Non np.float32 ) moneyness = m_ttm * np.sqrt(ttm) - ivs = np.interp(moneyness, mat.moneyness, mat.implied_vols) + ivs = np.interp(moneyness, mat.moneyness, mat.ivs) # drop any degenerate points (NaN / non-positive IV) valid = np.isfinite(ivs) & (ivs > 0) @@ -113,7 +113,7 @@ def _sample_day(rng: np.random.Generator, pricer: OptionPricer) -> DayData | Non return DayData( moneyness_ttm=np.concatenate(m_list), ttm=np.concatenate(t_list), - implied_vols=np.concatenate(iv_list), + ivs=np.concatenate(iv_list), ) diff --git a/quantflow/ai/tools/crypto.py b/quantflow/ai/tools/crypto.py index 52ef79e..a7662d2 100644 --- a/quantflow/ai/tools/crypto.py +++ b/quantflow/ai/tools/crypto.py @@ -68,7 +68,7 @@ async def crypto_implied_volatility(currency: str, maturity_index: int = -1) -> index = None if maturity_index < 0 else maturity_index vs.bs(index=index) df = vs.options_df(index=index) - df["implied_vol"] = df["implied_vol"].map("{:.2%}".format) + df["iv"] = df["iv"].map("{:.2%}".format) return df.to_csv(index=False) @mcp.tool() diff --git a/quantflow/options/calibration/base.py b/quantflow/options/calibration/base.py index 19d98e0..49ad465 100644 --- a/quantflow/options/calibration/base.py +++ b/quantflow/options/calibration/base.py @@ -52,10 +52,10 @@ class OptionEntry(BaseModel): options: list[OptionPrice] = Field(default_factory=list) """Bid and ask option prices for this entry""" - def implied_vol_range(self) -> Bounds: + def iv_range(self) -> Bounds: """Get the range of implied volatilities across bid and ask""" - implied_vols = tuple(option.implied_vol for option in self.options) - return Bounds(min(implied_vols), max(implied_vols)) + ivs = tuple(option.iv for option in self.options) + return Bounds(min(ivs), max(ivs)) def mid_price(self) -> float: """Mid price as the average of bid and ask call prices""" @@ -64,7 +64,7 @@ def mid_price(self) -> float: def mid_iv(self) -> float: """Mid implied volatility as the average of bid and ask""" - ivs = tuple(option.implied_vol for option in self.options) + ivs = tuple(option.iv for option in self.options) return sum(ivs) / len(ivs) @@ -175,17 +175,17 @@ def ref_date(self) -> datetime: return self.vol_surface.ref_date @property - def implied_vols(self) -> np.ndarray: + def ivs(self) -> np.ndarray: data: list[float] = [] for entry in self.options.values(): - data.extend(option.implied_vol for option in entry.options) + data.extend(option.iv for option in entry.options) return np.asarray(data) - def implied_vol_range(self) -> Bounds: + def iv_range(self) -> Bounds: """Range of implied volatilities across all calibration options""" return Bounds( - min(option.implied_vol_range().lb for option in self.options.values()), - max(option.implied_vol_range().ub for option in self.options.values()), + min(option.iv_range().lb for option in self.options.values()), + max(option.iv_range().ub for option in self.options.values()), ) def fit(self) -> OptimizeResult: diff --git a/quantflow/options/calibration/bns.py b/quantflow/options/calibration/bns.py index 36e3beb..bb92811 100644 --- a/quantflow/options/calibration/bns.py +++ b/quantflow/options/calibration/bns.py @@ -31,7 +31,7 @@ class BNSCalibration(VolModelCalibration[B], Generic[B]): """ def get_bounds(self) -> Bounds: - vol_range = self.implied_vol_range() + vol_range = self.iv_range() vol_lb = 0.5 * vol_range.lb[0] vol_ub = 1.5 * vol_range.ub[0] v2 = vol_lb**2 @@ -93,7 +93,7 @@ class BNS2Calibration(VolModelCalibration[B2], Generic[B2]): """ def get_bounds(self) -> Bounds: - vol_range = self.implied_vol_range() + vol_range = self.iv_range() vol_lb = 0.5 * vol_range.lb[0] vol_ub = 1.5 * vol_range.ub[0] v2 = vol_lb**2 diff --git a/quantflow/options/calibration/heston.py b/quantflow/options/calibration/heston.py index 94952c2..d6dd1a1 100644 --- a/quantflow/options/calibration/heston.py +++ b/quantflow/options/calibration/heston.py @@ -32,7 +32,7 @@ class HestonCalibration(VolModelCalibration[H], Generic[H]): ) def get_bounds(self) -> Bounds: - vol_range = self.implied_vol_range() + vol_range = self.iv_range() vol_lb = 0.5 * vol_range.lb[0] vol_ub = 1.5 * vol_range.ub[0] return Bounds( @@ -73,7 +73,7 @@ class HestonJCalibration(HestonCalibration[HestonJ[D]], Generic[D]): def get_bounds(self) -> Bounds: base = super().get_bounds() - vol_range = self.implied_vol_range() + vol_range = self.iv_range() vol_lb = 0.5 * vol_range.lb[0] vol_ub = 1.5 * vol_range.ub[0] lower = list(base.lb) + [1.0, (0.01 * vol_lb) ** 2] @@ -149,7 +149,7 @@ def maturity_split(self) -> float: return ttms[len(ttms) // 2] def get_bounds(self) -> Bounds: - vol_range = self.implied_vol_range() + vol_range = self.iv_range() vol_lb = 0.5 * vol_range.lb[0] vol_ub = 1.5 * vol_range.ub[0] v2 = vol_lb**2 @@ -271,7 +271,7 @@ class DoubleHestonJCalibration(DoubleHestonCalibration[DoubleHestonJ[D]], Generi def get_bounds(self) -> Bounds: base = super().get_bounds() - vol_range = self.implied_vol_range() + vol_range = self.iv_range() vol_lb = 0.5 * vol_range.lb[0] vol_ub = 1.5 * vol_range.ub[0] lower = list(base.lb) + [1.0, (0.01 * vol_lb) ** 2] diff --git a/quantflow/options/divfm/pricer.py b/quantflow/options/divfm/pricer.py index 01bfb4b..d4553eb 100644 --- a/quantflow/options/divfm/pricer.py +++ b/quantflow/options/divfm/pricer.py @@ -100,7 +100,7 @@ def calibrate( self, moneyness_ttm: FloatArray, ttm: FloatArray, - implied_vols: FloatArray, + ivs: FloatArray, extra: FloatArray | None = None, ) -> None: """Fit daily OLS coefficients from observed implied volatilities. @@ -118,7 +118,7 @@ def calibrate( Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau). ttm: Shape (N,). Time-to-maturity tau in years. - implied_vols: + ivs: Shape (N,). Observed implied volatilities. extra: Shape (N, extra_features) or None. Additional features passed to @@ -130,7 +130,7 @@ def calibrate( np.asarray(ttm, dtype=np.float32), extra_arr, ) - self.betas = np.linalg.lstsq(F, implied_vols, rcond=None)[0] + self.betas = np.linalg.lstsq(F, ivs, rcond=None)[0] # Store the mean X across options as the day-level representative value # used when pricing on a grid in _compute_maturity self.extra = ( diff --git a/quantflow/options/divfm/trainer.py b/quantflow/options/divfm/trainer.py index 5524916..5a9ed42 100644 --- a/quantflow/options/divfm/trainer.py +++ b/quantflow/options/divfm/trainer.py @@ -25,7 +25,7 @@ class DayData: """Shape (N,). Time-scaled moneyness M = log(K/F) / sqrt(tau).""" ttm: np.ndarray """Shape (N,). Time-to-maturity tau in years.""" - implied_vols: np.ndarray + ivs: np.ndarray """Shape (N,). Observed implied volatilities.""" extra: np.ndarray | None = None """Shape (N, extra_features) or None. Additional observable features X.""" @@ -44,7 +44,7 @@ def _day_loss( """ M = torch.tensor(day.moneyness_ttm, dtype=torch.float32) T = torch.tensor(day.ttm, dtype=torch.float32) - IV = torch.tensor(day.implied_vols, dtype=torch.float32) + IV = torch.tensor(day.ivs, dtype=torch.float32) extra = ( torch.tensor(day.extra, dtype=torch.float32) if day.extra is not None else None ) diff --git a/quantflow/options/pricer.py b/quantflow/options/pricer.py index 4fc71c9..19c5345 100644 --- a/quantflow/options/pricer.py +++ b/quantflow/options/pricer.py @@ -186,7 +186,7 @@ def prices(self, log_strikes: FloatArray) -> pd.DataFrame: "log_strike": log_strikes, "moneyness": self.moneyness(log_strikes), "call": call_prices, - "implied_vol": ivs, + "iv": ivs, "time_value": call_prices - np.maximum(0, 1 - np.exp(log_strikes)), } ) @@ -283,7 +283,7 @@ def plot3d( implied = np.zeros((len(ttm), len(moneyness))) for i, t in enumerate(ttm): maturity = self.maturity(cast(float, t)) - implied[i, :] = maturity.prices(moneyness * np.sqrt(t))["implied_vol"] + implied[i, :] = maturity.prices(moneyness * np.sqrt(t))["iv"] properties: dict = dict( xaxis_title="moneyness", yaxis_title="TTM", @@ -292,7 +292,7 @@ def plot3d( scene=dict( xaxis=dict(title="moneyness"), yaxis=dict(title="TTM"), - zaxis=dict(title="implied_vol"), + zaxis=dict(title="iv"), ), scene_camera=scene_camera or dict(eye=dict(x=1.2, y=-1.8, z=0.3)), contours=dict( diff --git a/quantflow/options/surface.py b/quantflow/options/surface.py index b13b0db..fe2d2b9 100644 --- a/quantflow/options/surface.py +++ b/quantflow/options/surface.py @@ -169,9 +169,7 @@ class OptionPrice(BaseModel): default=ZERO, description="Forward price of the underlying" ) ttm: float = Field(default=0, description="Time to maturity in years") - implied_vol: float = Field( - default=0, description="Implied volatility of the option" - ) + iv: float = Field(default=0, description="Implied volatility of the option") side: Side = Field( default=Side.bid, description="Side of the market for the option price" ) @@ -264,7 +262,7 @@ def calculate_price(self) -> Self: sigfig( black_price( np.asarray(self.log_strike), - self.implied_vol, + self.iv, self.ttm, 1 if self.option_type.is_call() else -1, ).sum(), @@ -286,7 +284,7 @@ def info_dict( log_strike=self.log_strike, moneyness=self.log_strike / np.sqrt(self.ttm), ttm=self.ttm, - implied_vol=self.implied_vol, + iv=self.iv, price=float(self.price_in_forward_space), price_bp=float(self.price_bp), price_quote=float(self.price_in_quote), @@ -310,7 +308,7 @@ def info( log_strike=to_decimal(self.log_strike), moneyness=to_decimal(self.log_strike / np.sqrt(self.ttm)), ttm=to_decimal(self.ttm), - implied_vol=to_decimal(self.implied_vol), + iv=to_decimal(self.iv), price=self.price_in_forward_space, price_bp=self.price_bp, price_quote=self.price_in_quote, @@ -336,7 +334,7 @@ class OptionInfo(BaseModel): description="Standardised moneyness, log(K/F) / sqrt(T)" ) ttm: DecimalNumber = Field(description="Time to maturity in years") - implied_vol: DecimalNumber = Field(description="Black implied volatility") + iv: DecimalNumber = Field(description="Black implied volatility") price: DecimalNumber = Field( description="Option price as a fraction of the forward price" ) @@ -360,7 +358,7 @@ class OptionArrays(NamedTuple): """The option prices""" ttm: FloatArray """Time to maturity of the options""" - implied_vol: FloatArray + iv: FloatArray """Implied volatility of the options""" call_put: FloatArray """Indicator for call (1) or put (-1) options""" @@ -403,11 +401,11 @@ def price(self) -> Price: def iv_bid_ask_spread(self) -> float: """Calculate the bid-ask spread of the implied volatility""" - return self.ask.implied_vol - self.bid.implied_vol + return self.ask.iv - self.bid.iv def iv_mid(self) -> float: """Calculate the mid implied volatility""" - return (self.bid.implied_vol + self.ask.implied_vol) / 2 + return (self.bid.iv + self.ask.iv) / 2 def is_in_the_money(self, forward: Decimal) -> bool: """Check if the option is in the money given the forward price""" @@ -431,8 +429,8 @@ def prices( for o in (self.bid, self.ask): o.forward = forward o.ttm = ttm - if not o.implied_vol: - o.implied_vol = initial_vol + if not o.iv: + o.iv = initial_vol yield o def inputs(self) -> OptionInput: @@ -446,14 +444,10 @@ def inputs(self) -> OptionInput: maturity=self.meta.maturity, option_type=self.meta.option_type, iv_bid=to_decimal_or_none( - None - if np.isnan(self.bid.implied_vol) - else round(self.bid.implied_vol, 7) + None if np.isnan(self.bid.iv) else round(self.bid.iv, 7) ), iv_ask=to_decimal_or_none( - None - if np.isnan(self.ask.implied_vol) - else round(self.ask.implied_vol, 7) + None if np.isnan(self.ask.iv) else round(self.ask.iv, 7) ), ) @@ -746,7 +740,7 @@ def disable_outliers( svi = SVI.fit(log_m, iv_mid, ttm) except Exception: break - iv_fit = svi.implied_vol(log_m, ttm) + iv_fit = svi.iv(log_m, ttm) residuals = np.abs(iv_mid - iv_fit) / iv_mid found = False for option, residual in zip(options, residuals): @@ -1026,14 +1020,12 @@ def bs( k=d.log_strike, price=d.price, ttm=d.ttm, - initial_sigma=d.implied_vol, + initial_sigma=d.iv, call_put=d.call_put, ) - for option, implied_vol, converged in zip( - d.options, result.values, result.converged - ): - option.implied_vol = float(implied_vol) - option.converged = converged and not np.isnan(implied_vol) + for option, iv, converged in zip(d.options, result.values, result.converged): + option.iv = float(iv) + option.converged = converged and not np.isnan(iv) return d.options def calc_bs_prices( @@ -1052,7 +1044,7 @@ def calc_bs_prices( otherwise the price calculation won't be correct. """ d = self.as_array(select=select, index=index, converged=True) - return black_price(k=d.log_strike, sigma=d.implied_vol, ttm=d.ttm, s=d.call_put) + return black_price(k=d.log_strike, sigma=d.iv, ttm=d.ttm, s=d.call_put) def options_df( self, @@ -1123,14 +1115,14 @@ def as_array( log_strike.append(float(option.log_strike)) price.append(float(option.price_in_forward_space)) ttm.append(float(option.ttm)) - vol.append(float(option.implied_vol)) + vol.append(float(option.iv)) call_put.append(1 if option.option_type.is_call() else -1) return OptionArrays( options=options, log_strike=np.array(log_strike), price=np.array(price), ttm=np.array(ttm), - implied_vol=np.array(vol), + iv=np.array(vol), call_put=np.array(call_put), ) diff --git a/quantflow/options/svi.py b/quantflow/options/svi.py index 1215552..e8aeb6b 100644 --- a/quantflow/options/svi.py +++ b/quantflow/options/svi.py @@ -88,7 +88,7 @@ def total_variance( km = k_arr - m return a + b * (rho * km + np.sqrt(km**2 + theta**2)) - def implied_vol( + def iv( self, k: Annotated[ArrayLike, Doc("Log-moneyness log(K/F), scalar or array")], ttm: Annotated[float, Doc("Time to maturity in years")], diff --git a/quantflow/utils/plot.py b/quantflow/utils/plot.py index 17357db..e5bc45e 100644 --- a/quantflow/utils/plot.py +++ b/quantflow/utils/plot.py @@ -111,7 +111,7 @@ def plot_vol_surface( model: pd.DataFrame | None = None, marker_size: int = 10, x_series: str = "moneyness", - series: str = "implied_vol", + series: str = "iv", color_series: str = "side", fig: Any | None = None, fig_params: dict | None = None, @@ -153,7 +153,7 @@ def plot_vol_surface( def plot_vol_surface_3d( df: pd.DataFrame, *, - series: str = "implied_vol", + series: str = "iv", fig: Any | None = None, dragmode: str = "turntable", uirevision: str = "preserve_ui_state", @@ -173,7 +173,7 @@ def plot_vol_cross( data: pd.DataFrame, *, data2: pd.DataFrame | None = None, - series: str = "implied_vol", + series: str = "iv", marker_size: int = 10, fig: Any | None = None, name: str = "model", diff --git a/quantflow_tests/test_ai.py b/quantflow_tests/test_ai.py index ebd2596..41d0999 100644 --- a/quantflow_tests/test_ai.py +++ b/quantflow_tests/test_ai.py @@ -325,7 +325,7 @@ async def test_crypto_implied_volatility( result = await crypto_server.call_tool( "crypto_implied_volatility", {"currency": "ETH"} ) - assert "implied_vol" in text(result) + assert "iv" in text(result) async def test_crypto_prices( diff --git a/quantflow_tests/test_disable_outliers.py b/quantflow_tests/test_disable_outliers.py index adb8bcb..726dac0 100644 --- a/quantflow_tests/test_disable_outliers.py +++ b/quantflow_tests/test_disable_outliers.py @@ -46,7 +46,7 @@ def _make_option( meta=meta, forward=FORWARD, ttm=TTM, - implied_vol=iv_bid, + iv=iv_bid, side=Side.bid, converged=True, ) @@ -55,7 +55,7 @@ def _make_option( meta=meta, forward=FORWARD, ttm=TTM, - implied_vol=iv_ask, + iv=iv_ask, side=Side.ask, converged=True, ) @@ -150,7 +150,7 @@ def _svi_smile( theta=Decimal("0.15"), ) k = np.log(np.array(strikes) / forward) - return svi.implied_vol(k, ttm).tolist() + return svi.iv(k, ttm).tolist() def test_pass2_disables_svi_outlier() -> None: diff --git a/quantflow_tests/test_divfm.py b/quantflow_tests/test_divfm.py index c2214b9..bb0b841 100644 --- a/quantflow_tests/test_divfm.py +++ b/quantflow_tests/test_divfm.py @@ -79,8 +79,8 @@ def _synthetic_options( rng = np.random.default_rng(42) moneyness_ttm = rng.uniform(-2.0, 2.0, n).astype(np.float32) ttm = rng.uniform(0.1, 2.0, n).astype(np.float32) - implied_vols = np.full(n, iv, dtype=np.float64) - return moneyness_ttm, ttm, implied_vols + ivs = np.full(n, iv, dtype=np.float64) + return moneyness_ttm, ttm, ivs # --------------------------------------------------------------------------- @@ -106,8 +106,8 @@ def test_first_factor_is_constant_one(weights: DIVFMWeights) -> None: def test_calibrate_constant_iv(pricer: DIVFMPricer) -> None: target_iv = 0.3 - moneyness_ttm, ttm, implied_vols = _synthetic_options(iv=target_iv) - pricer.calibrate(moneyness_ttm, ttm, implied_vols) + moneyness_ttm, ttm, ivs = _synthetic_options(iv=target_iv) + pricer.calibrate(moneyness_ttm, ttm, ivs) # With zero-weight subnets, all non-constant factors collapse to 0, # so the model reduces to sigma = beta_1 * 1 = mean(IV) @@ -118,8 +118,8 @@ def test_calibrate_constant_iv(pricer: DIVFMPricer) -> None: def test_maturity_after_calibrate(weights: DIVFMWeights) -> None: # Use a tighter moneyness range so Black-Scholes IV inversion is reliable pricer = DIVFMPricer(weights=weights) - moneyness_ttm, ttm, implied_vols = _synthetic_options(iv=0.3) - pricer.calibrate(moneyness_ttm, ttm, implied_vols) + moneyness_ttm, ttm, ivs = _synthetic_options(iv=0.3) + pricer.calibrate(moneyness_ttm, ttm, ivs) mp = pricer.maturity(0.5) assert mp.ttm == pytest.approx(0.5, rel=1e-3) @@ -130,12 +130,12 @@ def test_maturity_after_calibrate(weights: DIVFMWeights) -> None: assert np.all(df["call"] >= 0.0) # implied vols should be close to the calibrated constant in the central region central = np.abs(df["moneyness"]) < 0.5 - assert np.allclose(df.loc[central, "implied_vol"], 0.3, atol=1e-3) + assert np.allclose(df.loc[central, "iv"], 0.3, atol=1e-3) def test_maturity_cache(pricer: DIVFMPricer) -> None: - moneyness_ttm, ttm, implied_vols = _synthetic_options() - pricer.calibrate(moneyness_ttm, ttm, implied_vols) + moneyness_ttm, ttm, ivs = _synthetic_options() + pricer.calibrate(moneyness_ttm, ttm, ivs) mp1 = pricer.maturity(0.25) mp2 = pricer.maturity(0.25) @@ -155,10 +155,10 @@ def test_calibrate_with_extra(weights: DIVFMWeights) -> None: extra_features=extra_features, ) pricer = DIVFMPricer(weights=w) - moneyness_ttm, ttm, implied_vols = _synthetic_options() + moneyness_ttm, ttm, ivs = _synthetic_options() extra = np.zeros((N, extra_features), dtype=np.float32) - pricer.calibrate(moneyness_ttm, ttm, implied_vols, extra=extra) + pricer.calibrate(moneyness_ttm, ttm, ivs, extra=extra) assert pricer.extra is not None assert pricer.extra.shape == (1, extra_features) @@ -170,8 +170,8 @@ def test_calibrate_with_extra(weights: DIVFMWeights) -> None: def test_reset_clears_cache(pricer: DIVFMPricer) -> None: - moneyness_ttm, ttm, implied_vols = _synthetic_options() - pricer.calibrate(moneyness_ttm, ttm, implied_vols) + moneyness_ttm, ttm, ivs = _synthetic_options() + pricer.calibrate(moneyness_ttm, ttm, ivs) pricer.maturity(0.25) assert len(pricer.ttm) == 1 @@ -237,10 +237,10 @@ def _make_days( n = int(rng.integers(20, 60)) m = rng.uniform(-2.0, 2.0, n).astype(np.float32) t = rng.uniform(0.1, 2.0, n).astype(np.float32) - implied_vols = ( + ivs = ( iv_fn(m, t) if callable(iv_fn) else np.full(n, iv, dtype=np.float64) # type: ignore[operator] ) - days.append(DayData(moneyness_ttm=m, ttm=t, implied_vols=implied_vols)) + days.append(DayData(moneyness_ttm=m, ttm=t, ivs=ivs)) return days @@ -308,7 +308,7 @@ def test_trainer_to_weights_produces_pricer() -> None: pricer = DIVFMPricer(weights=weights) day = days[0] - pricer.calibrate(day.moneyness_ttm, day.ttm, day.implied_vols) + pricer.calibrate(day.moneyness_ttm, day.ttm, day.ivs) mp = pricer.maturity(0.5) df = mp.prices(np.linspace(-1.5, 1.5, 20) * np.sqrt(mp.ttm)) assert len(df) == 20 diff --git a/quantflow_tests/test_non_inverse_surface.py b/quantflow_tests/test_non_inverse_surface.py index 6689b09..824231d 100644 --- a/quantflow_tests/test_non_inverse_surface.py +++ b/quantflow_tests/test_non_inverse_surface.py @@ -84,7 +84,7 @@ def test_bs_recovers_input_volatility() -> None: options = list(surface.option_prices(converged=True)) assert options, "expected converged options on the synthetic surface" for option in options: - assert option.implied_vol == pytest.approx(SIGMA, abs=5e-4) + assert option.iv == pytest.approx(SIGMA, abs=5e-4) def test_non_inverse_price_in_forward_space_matches_black() -> None: diff --git a/quantflow_tests/test_options.py b/quantflow_tests/test_options.py index 1823166..f88cf53 100644 --- a/quantflow_tests/test_options.py +++ b/quantflow_tests/test_options.py @@ -47,7 +47,7 @@ def _make_option( day_counter = DayCounter.ACTACT return OptionPrice( price=Decimal(str(price)), - implied_vol=0.5, + iv=0.5, forward=Decimal(str(forward)), ttm=day_counter.dcf(ref_date, maturity), meta=OptionMetadata( @@ -152,7 +152,7 @@ def test_trim_full(vol_surface: VolSurface) -> None: assert trimmed == vol_surface -def test_inputs_implied_vols(vol_surface: VolSurface) -> None: +def test_inputs_ivs(vol_surface: VolSurface) -> None: vol_surface.bs() inputs = vol_surface.inputs() option_inputs = [i for i in inputs.inputs if isinstance(i, OptionInput)] @@ -164,7 +164,7 @@ def test_inputs_implied_vols(vol_surface: VolSurface) -> None: assert converged -def test_inputs_implied_vols_rounded(vol_surface: VolSurface) -> None: +def test_inputs_ivs_rounded(vol_surface: VolSurface) -> None: vol_surface.bs() inputs = vol_surface.inputs() option_inputs = [i for i in inputs.inputs if isinstance(i, OptionInput)] @@ -222,7 +222,7 @@ def test_calibration_setup(vol_surface: VolSurface, heston: OptionPricer[Heston] ) assert cal.ref_date == vol_surface.ref_date assert cal.options - vol_range = cal.implied_vol_range() + vol_range = cal.iv_range() assert vol_range.lb < vol_range.ub assert vol_range.lb > 0 assert vol_range.ub < 10 diff --git a/quantflow_tests/test_svi.py b/quantflow_tests/test_svi.py index 450d7cd..1465ec1 100644 --- a/quantflow_tests/test_svi.py +++ b/quantflow_tests/test_svi.py @@ -51,35 +51,35 @@ def test_total_variance_scalar_input() -> None: # --------------------------------------------------------------------------- -# implied_vol +# iv # --------------------------------------------------------------------------- -def test_implied_vol_non_negative() -> None: +def test_iv_non_negative() -> None: svi = make_svi() - iv = svi.implied_vol(K, TTM) + iv = svi.iv(K, TTM) assert np.all(iv >= 0) -def test_implied_vol_zero_where_variance_non_positive() -> None: +def test_iv_zero_where_variance_non_positive() -> None: # force a very negative a so some w(k) <= 0 svi = make_svi(a=-0.5, b=0.01) - iv = svi.implied_vol(K, TTM) + iv = svi.iv(K, TTM) w = svi.total_variance(K) assert np.all(iv[w <= 0] == 0.0) -def test_implied_vol_consistent_with_total_variance() -> None: +def test_iv_consistent_with_total_variance() -> None: svi = make_svi() - iv = svi.implied_vol(K, TTM) + iv = svi.iv(K, TTM) w = svi.total_variance(K) assert np.allclose(iv**2 * TTM, w) -def test_implied_vol_scales_with_ttm() -> None: +def test_iv_scales_with_ttm() -> None: svi = make_svi() - iv1 = svi.implied_vol(K, 1.0) - iv2 = svi.implied_vol(K, 0.25) + iv1 = svi.iv(K, 1.0) + iv2 = svi.iv(K, 0.25) # same total variance, shorter ttm => higher iv assert np.all(iv2 > iv1) @@ -90,7 +90,7 @@ def test_implied_vol_scales_with_ttm() -> None: def _synthetic_iv(svi: SVI, k: np.ndarray, ttm: float) -> np.ndarray: - return svi.implied_vol(k, ttm) + return svi.iv(k, ttm) def test_fit_recovers_parameters() -> None: @@ -104,18 +104,18 @@ def test_fit_recovers_parameters() -> None: assert float(fitted.theta) == pytest.approx(float(true_svi.theta), abs=1e-4) -def test_fit_reproduces_implied_vols() -> None: +def test_fit_reproduces_ivs() -> None: true_svi = make_svi() iv_obs = _synthetic_iv(true_svi, K, TTM) fitted = SVI.fit(K, iv_obs, TTM) - iv_fit = fitted.implied_vol(K, TTM) + iv_fit = fitted.iv(K, TTM) assert np.allclose(iv_fit, iv_obs, atol=1e-5) def test_fit_flat_smile() -> None: iv_obs = np.full_like(K, 0.20) fitted = SVI.fit(K, iv_obs, TTM) - iv_fit = fitted.implied_vol(K, TTM) + iv_fit = fitted.iv(K, TTM) assert np.allclose(iv_fit, 0.20, atol=1e-4) @@ -132,5 +132,5 @@ def test_fit_skewed_smile() -> None: true_svi = make_svi(rho=-0.5, m=-0.1) iv_obs = _synthetic_iv(true_svi, K, TTM) fitted = SVI.fit(K, iv_obs, TTM) - iv_fit = fitted.implied_vol(K, TTM) + iv_fit = fitted.iv(K, TTM) assert np.allclose(iv_fit, iv_obs, atol=1e-5)