Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/api/heston.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
)

Expand Down Expand Up @@ -68,13 +68,13 @@ 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

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],
)
4 changes: 2 additions & 2 deletions app/scripts/heston_divfm_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
)


Expand Down
6 changes: 3 additions & 3 deletions docs/examples/pricing_method_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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(".", "_")
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/heston-vol-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"}),
]
}));
Expand All @@ -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,
Expand All @@ -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}),
]
}));
```
10 changes: 5 additions & 5 deletions frontend/src/volatility-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions notebooks/heston_divfm_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
)


Expand Down
2 changes: 1 addition & 1 deletion quantflow/ai/tools/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 9 additions & 9 deletions quantflow/options/calibration/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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)


Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions quantflow/options/calibration/bns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions quantflow/options/calibration/heston.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions quantflow/options/divfm/pricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 = (
Expand Down
4 changes: 2 additions & 2 deletions quantflow/options/divfm/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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
)
Expand Down
6 changes: 3 additions & 3 deletions quantflow/options/pricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
}
)
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down
Loading
Loading