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
4 changes: 2 additions & 2 deletions app/api/hurst.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _range_std(pdf: Any, range_seconds: float, seconds_in_day: int) -> float:

def _ohlc_hurst(df: pd.DataFrame, serie: str, periods: tuple) -> dict[str, float]:
template = OHLC(
serie=serie,
series=serie,
period="10m",
rogers_satchell_variance=True,
parkinson_variance=True,
Expand Down Expand Up @@ -97,7 +97,7 @@ async def hurst_wiener(
gk_vals = []
rs_vals = []
template = OHLC(
serie="0",
series="0",
period="10m",
rogers_satchell_variance=True,
parkinson_variance=True,
Expand Down
10 changes: 10 additions & 0 deletions docs/api/options/moneyness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Moneyness

Conversion utilities between strike, log-strike, and
[moneyness](../../glossary.md#moneyness) representations.

These functions provide a single authoritative source for the relationships
between absolute strikes, log-strikes, and time-scaled moneyness used
throughout the library.

::: quantflow.options.moneyness
6 changes: 2 additions & 4 deletions docs/api/options/vol_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

## Bid/Ask Prices

::: quantflow.options.surface.Price

::: quantflow.options.surface.SpotPrice

::: quantflow.options.surface.FwdPrice
Expand All @@ -27,8 +25,6 @@

::: quantflow.options.surface.OptionArrays

::: quantflow.options.surface.OptionMetadata

::: quantflow.options.surface.OptionPrice

::: quantflow.options.surface.OptionPrices
Expand All @@ -41,6 +37,8 @@

::: quantflow.options.inputs.OptionType

::: quantflow.options.inputs.OptionMetadata

::: quantflow.options.inputs.VolSurfaceInputs

::: quantflow.options.inputs.VolSurfaceInput
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ta/ewma.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# EWMA


::: quantflow.ta.EWMA
::: quantflow.ta.ewma.EWMA
2 changes: 1 addition & 1 deletion docs/api/ta/kalman.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Kalman Filter

::: quantflow.ta.KalmanFilter
::: quantflow.ta.kalman.KalmanFilter
2 changes: 1 addition & 1 deletion docs/api/ta/supersmoother.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Reference:
Ehlers, J. (2013). "Cycle Analytics for Traders"


::: quantflow.ta.SuperSmoother
::: quantflow.ta.supersmoother.SuperSmoother
5 changes: 5 additions & 0 deletions docs/api/utils/price.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Bid/Ask Prices

::: quantflow.utils.price.Price

::: quantflow.utils.price.PriceVolume
4 changes: 3 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ nav:
- Black-Scholes: api/options/black.md
- Calibration: api/options/calibration.md
- Deep IV Factor Model: api/options/divfm.md
- Moneyness: api/options/moneyness.md
- Put-Call Parity: api/options/parity.md
- Pricer: api/options/pricer.md
- SVI Smile: api/options/svi.md
Expand All @@ -90,7 +91,7 @@ nav:
- Poisson Process: api/sp/poisson.md
- Wiener Process: api/sp/wiener.md
- Copulas: api/sp/copula.md
- Technical Analysis:
- Timeseries Analysis:
- api/ta/index.md
- EWMA: api/ta/ewma.md
- Kalman Filter: api/ta/kalman.md
Expand All @@ -109,6 +110,7 @@ nav:
- Distributions: api/utils/distributions.md
- Marginal 1D: api/utils/marginal1d.md
- Numbers: api/utils/numbers.md
- Price: api/utils/price.md
- Types: api/utils/types.md
- Tutorials:
- tutorials/index.md
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ readme = "readme.md"
requires-python = ">=3.11,<3.15"
dependencies = [
"scipy>=1.14.1",
"pandas>=2.0.0",
"pydantic>=2.0.2",
"ccy>=2.0.0",
"python-dotenv>=1.0.1",
"polars[pandas,pyarrow]>=1.11.0",
"statsmodels>=0.14.6,<0.15.0",
]

Expand Down
13 changes: 10 additions & 3 deletions quantflow/options/calibration/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from quantflow.utils import plot

from ..bs import implied_black_volatility
from ..moneyness import log_strike_from_moneyness, moneyness_from_log_strike
from ..pricer import OptionPricerBase
from ..surface import OptionPrice, VolSurface

Expand Down Expand Up @@ -150,7 +151,9 @@ def model_post_init(self, _ctx: Any) -> None:
entries = tuple(self.options.values())
self._log_strikes = np.asarray([e.log_strike for e in entries])
self._ttms = np.asarray([e.ttm for e in entries])
self._moneyness = self._log_strikes / np.sqrt(self._ttms)
self._moneyness = np.asarray(
moneyness_from_log_strike(self._log_strikes, self._ttms)
)
self._mid_prices = np.asarray([e.mid_price() for e in entries])
self._mid_ivs = np.asarray([e.mid_iv() for e in entries])

Expand Down Expand Up @@ -223,7 +226,7 @@ def cost_weight(self, ttm: float, log_strike: float) -> float:
Up-weights wing options via `exp(moneyness_weight * moneyness**2)`,
capped at `max_cost_weight`. The quadratic form mimics `1/vega`.
"""
moneyness = log_strike / np.sqrt(ttm)
moneyness = float(moneyness_from_log_strike(log_strike, ttm))
weight = np.exp(self.moneyness_weight * moneyness * moneyness)
return float(min(weight, self.max_cost_weight))

Expand Down Expand Up @@ -281,7 +284,11 @@ def cost_function(self, params: np.ndarray) -> float:
def _model_grid(
self, ttm: float, max_moneyness: float, support: int
) -> pd.DataFrame:
log_strikes = np.linspace(-max_moneyness, max_moneyness, support) * np.sqrt(ttm)
log_strikes = np.asarray(
log_strike_from_moneyness(
np.linspace(-max_moneyness, max_moneyness, support), ttm
)
)
return self.pricer.maturity(ttm).prices(log_strikes)

def plot(
Expand Down
61 changes: 36 additions & 25 deletions quantflow/options/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import enum
from datetime import datetime
from decimal import Decimal
from typing import Self, TypeVar

from pydantic import BaseModel, Field
from typing_extensions import Annotated, Doc

from quantflow.rates import AnyYieldCurve
from quantflow.utils.numbers import ZERO, DecimalNumber
from quantflow.utils.numbers import DecimalNumber
from quantflow.utils.price import PriceVolume

P = TypeVar("P")

Expand Down Expand Up @@ -36,6 +39,29 @@ def call_put(self) -> int:
return 1 if self is OptionType.call else -1


class OptionMetadata(BaseModel):
"""Represents the metadata of an option, including its strike, type, maturity,
and other relevant information."""

strike: DecimalNumber = Field(description="Strike price of the option")
option_type: OptionType = Field(description="Type of the option, call or put")
maturity: datetime = Field(description="Maturity date of the option")
inverse: bool = Field(
default=True,
description=(
"Whether the option is an inverse option (i.e. quoted in terms of the "
"underlying) or not (i.e. quoted in terms of the quote currency)"
),
)

def is_in_the_money(self, forward: Decimal) -> bool:
"""Check if the option is in the money given the forward price"""
if self.option_type.is_call():
return self.strike < forward
else:
return self.strike > forward


class VolSecurityType(enum.StrEnum):
"""Type of security for the volatility surface"""

Expand Down Expand Up @@ -78,18 +104,7 @@ def option(cls) -> Self:
return cls(security_type=VolSecurityType.option)


class VolSurfaceInput(BaseModel):
"""Base class for volatility surface inputs"""

bid: DecimalNumber = Field(description="Bid price of the security")
ask: DecimalNumber = Field(description="Ask price of the security")
open_interest: DecimalNumber = Field(
default=ZERO, description="Open interest of the security"
)
volume: DecimalNumber = Field(default=ZERO, description="Volume of the security")


class SpotInput(VolSurfaceInput):
class SpotInput(PriceVolume):
"""Input data for a spot contract in the volatility surface"""

security_type: VolSecurityType = Field(
Expand All @@ -98,7 +113,7 @@ class SpotInput(VolSurfaceInput):
)


class ForwardInput(VolSurfaceInput):
class ForwardInput(PriceVolume):
"""Input data for a forward contract in the volatility surface"""

maturity: datetime = Field(description="Expiry date of the forward contract")
Expand All @@ -108,12 +123,9 @@ class ForwardInput(VolSurfaceInput):
)


class OptionInput(VolSurfaceInput):
class OptionInput(PriceVolume, OptionMetadata):
"""Input data for an option in the volatility surface"""

strike: DecimalNumber = Field(description="Strike price of the option")
maturity: datetime = Field(description="Expiry date of the option")
option_type: OptionType = Field(description="Type of the option - call or put")
security_type: VolSecurityType = Field(
default=VolSecurityType.option,
description="Type of security for the volatility surface",
Expand All @@ -132,13 +144,12 @@ class OptionInput(VolSurfaceInput):
"(e.g. 0.2 for 20%)"
),
)
inverse: bool = Field(
default=True,
description=(
"Whether the security is inverse (i.e. quoted in terms of the underlying) "
"or not (i.e. quoted in terms of the quote currency)"
),
)


VolSurfaceInput = Annotated[
SpotInput | ForwardInput | OptionInput,
Doc("Input data for a security in the volatility surface"),
]


class VolSurfaceInputs(BaseModel):
Expand Down
84 changes: 84 additions & 0 deletions quantflow/options/moneyness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Moneyness and log-strike conversion utilities.

This module provides the canonical conversions between absolute strikes,
log-strikes, and time-scaled moneyness used throughout the library.

See the [glossary](../glossary.md#moneyness) for definitions.
"""

from __future__ import annotations

import math
from decimal import Decimal

import numpy as np
from typing_extensions import Annotated, Doc

from quantflow.utils.numbers import to_decimal
from quantflow.utils.types import FloatArrayLike


def moneyness_from_log_strike(
log_strike: Annotated[FloatArrayLike, Doc("Log-strike")],
ttm: Annotated[FloatArrayLike, Doc("Time to maturity in years")],
) -> FloatArrayLike:
r"""Convert log-strike $k$ to moneyness $m$.

\begin{equation}
m = \frac{k}{\sqrt{\tau}}
\end{equation}
"""
return log_strike / np.sqrt(ttm)


def log_strike_from_moneyness(
moneyness: Annotated[FloatArrayLike, Doc("Time-scaled moneyness")],
ttm: Annotated[FloatArrayLike, Doc("Time to maturity in years")],
) -> FloatArrayLike:
r"""Convert time-scaled moneyness to log-strike.

\begin{equation}
k = m \sqrt{\tau}
\end{equation}
"""
return moneyness * np.sqrt(ttm)


def strike_from_moneyness(
moneyness: Annotated[float, Doc("Time-scaled moneyness")],
ttm: Annotated[float, Doc("Time to maturity in years")],
forward: Annotated[float, Doc("Forward price of the underlying")],
) -> Decimal:
r"""Convert time-scaled moneyness to an absolute strike price.

\begin{equation}
K = F \exp\left(m \sqrt{\tau}\right)
\end{equation}
"""
return to_decimal(forward * math.exp(moneyness * math.sqrt(ttm)))


def strike_from_log_strike(
log_strike: Annotated[float, Doc("Log-strike")],
forward: Annotated[float, Doc("Forward price of the underlying")],
) -> Decimal:
r"""Convert log-strike to an absolute strike price.

\begin{equation}
K = F \exp(k)
\end{equation}
"""
return to_decimal(forward * math.exp(log_strike))


def log_strike_from_strike(
strike: Annotated[float | Decimal, Doc("Absolute strike price")],
forward: Annotated[float | Decimal, Doc("Forward price of the underlying")],
) -> float:
r"""Convert an absolute strike to log-strike.

\begin{equation}
k = \ln\frac{K}{F}
\end{equation}
"""
return math.log(float(strike) / float(forward))
11 changes: 7 additions & 4 deletions quantflow/options/pricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..utils.types import FloatArray, FloatArrayLike
from .bs import BlackSensitivities, implied_black_volatility
from .inputs import OptionType
from .moneyness import log_strike_from_moneyness, moneyness_from_log_strike

M = TypeVar("M", bound=StochasticProcess1D)

Expand Down Expand Up @@ -143,7 +144,7 @@ class MaturityPricer(BaseModel, arbitrary_types_allowed=True):

def moneyness(self, log_strikes: FloatArrayLike) -> FloatArrayLike:
"""Time-adjusted moneyness for one or many log-strikes"""
return log_strikes / np.sqrt(self.ttm)
return moneyness_from_log_strike(log_strikes, self.ttm)

def price(
self,
Expand Down Expand Up @@ -237,8 +238,8 @@ def price(
self,
option_type: Annotated[OptionType, Doc("Type of the option (call or put)")],
ttm: Annotated[float, Doc("Time to maturity")],
strike: Annotated[float, Doc("Strike price of the option")],
forward: Annotated[float, Doc("Forward price of the underlying")],
strike: Annotated[float | Decimal, Doc("Strike price of the option")],
forward: Annotated[float | Decimal, Doc("Forward price of the underlying")],
) -> ModelOptionPrice:
"""Price a single option

Expand Down Expand Up @@ -283,7 +284,9 @@ 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))["iv"]
implied[i, :] = maturity.prices(
np.asarray(log_strike_from_moneyness(moneyness, t))
)["iv"]
properties: dict = dict(
xaxis_title="moneyness",
yaxis_title="TTM",
Expand Down
Loading
Loading