Skip to content

Vol Surface

quantflow.options.surface.VolSurface pydantic-model

Bases: ForwardPricer[S]

Represents a volatility surface, which captures the implied volatility of an option for different strikes and maturities.

Key Concepts:

  • Implied Volatility: The market's expectation of future volatility, derived from the price of an option using a pricing model (e.g., Black-Scholes).
  • Strike Price: The price at which the underlying asset can be bought (call option) or sold (put option) at the option's expiry.
  • Time to Maturity: The time remaining until the option's expiration date.
  • Volatility Smile/Skew: The often-observed phenomenon where implied volatility varies across different strike prices for the same maturity. Typically, it forms a "smile" or "skew" shape.

This class provides a structure for storing and manipulating volatility surface data. It can be used for various tasks, such as:

  • Option pricing and risk management: Using the surface to determine the appropriate volatility input for pricing models.
  • Volatility arbitrage: Identifying mispricings in options by comparing market prices to model prices derived from the surface.
  • Market analysis: Understanding market sentiment and expectations of future volatility.

Fields:

maturities pydantic-field

maturities = ()

Sorted tuple of VolCrossSection, each containing the forward price and option prices for that maturity

asset pydantic-field

asset = ''

Name of the underlying asset

spot pydantic-field

spot = None

Spot price of the underlying asset

quote_curve pydantic-field

quote_curve

Discount curve for the quote

asset_curve pydantic-field

asset_curve

Discount curve for the asset

tick_size_forwards pydantic-field

tick_size_forwards = None

Tick size for rounding forward and spot prices - optional

tick_size_options pydantic-field

tick_size_options = None

Tick size for rounding option prices - optional

day_counter pydantic-field

day_counter = default_day_counter

Day counter for time to maturity calculations, by default it uses Act/Act

ref_date property

ref_date

Reference date for the volatility surface, taken as the earliest maturity or the provided ref_date if it's earlier

securities

securities(*, select=ALL, index=None, converged=False)

Iterator over securities in the volatility surface

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: ALL

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

converged

Include the spot, forwards and options with implied volatility converged only if True, otherwise include all securities regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def securities(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.ALL,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    converged: Annotated[
        bool,
        Doc(
            "Include the spot, forwards and options with implied volatility "
            "converged only if True, otherwise include all securities regardless "
            "of convergence"
        ),
    ] = False,
) -> Iterator[SpotPrice[S] | FwdPrice[S] | OptionPrices[S]]:
    """Iterator over securities in the volatility surface"""
    if self.spot is not None:
        yield self.spot
    if index is not None:
        yield from self.maturities[index].securities(
            select=select, converged=converged
        )
    else:
        for maturity in self.maturities:
            yield from maturity.securities(select=select, converged=converged)

inputs

inputs(*, select=ALL, index=None, converged=False)

Convert the volatility surface to a VolSurfaceInputs instance

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: ALL

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

converged

Include spot, forwards and options with implied volatility converged only if True, otherwise include all securities regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def inputs(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.ALL,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    converged: Annotated[
        bool,
        Doc(
            "Include spot, forwards and options with implied volatility "
            "converged only if True, otherwise include all securities regardless "
            "of convergence"
        ),
    ] = False,
) -> VolSurfaceInputs:
    """Convert the volatility surface to a
    [VolSurfaceInputs][quantflow.options.inputs.VolSurfaceInputs] instance"""
    return VolSurfaceInputs(
        asset=self.asset,
        asset_curve=self.asset_curve,
        quote_curve=self.quote_curve,
        inputs=list(
            s.inputs()
            for s in self.securities(
                select=select, converged=converged, index=index
            )
        ),
    )

term_structure

term_structure()

Return the term structure of the volatility surface as a DataFrame

Source code in quantflow/options/surface.py
def term_structure(self) -> pd.DataFrame:
    """Return the term structure of the volatility surface as a DataFrame"""
    spot = self.spot_price()
    return pd.DataFrame(
        cross.info_dict(self.ref_date, spot, self.forward(cross.maturity))
        for cross in self.maturities
    )

trim

trim(num_maturities)

Create a new volatility surface with the last num_maturities maturities

Source code in quantflow/options/surface.py
def trim(self, num_maturities: int) -> Self:
    """Create a new volatility surface with the last `num_maturities` maturities"""
    return self.model_copy(
        update=dict(maturities=self.maturities[-num_maturities:])
    )

option_prices

option_prices(
    *,
    select=BEST,
    index=None,
    initial_vol=INITIAL_VOL,
    converged=False
)

Iterator over selected option prices in the surface

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

converged

Include options with implied volatility converged only if True, otherwise include all options regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def option_prices(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
    converged: Annotated[
        bool,
        Doc(
            "Include options with implied volatility "
            "converged only if True, otherwise include all options regardless "
            "of convergence"
        ),
    ] = False,
) -> Iterator[OptionPrice]:
    """Iterator over selected option prices in the surface"""
    if index is not None:
        cross = self.maturities[index]
        yield from cross.option_prices(
            self.ref_date,
            self.forward(cross.maturity),
            select=select,
            initial_vol=initial_vol,
            converged=converged,
        )
    else:
        for cross in self.maturities:
            yield from cross.option_prices(
                self.ref_date,
                self.forward(cross.maturity),
                select=select,
                initial_vol=initial_vol,
                converged=converged,
            )

option_list

option_list(*, select=BEST, index=None, converged=False)

List of selected option prices in the surface

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

converged

Include options with implied volatility converged only if True, otherwise include all options regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def option_list(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    converged: Annotated[
        bool,
        Doc(
            "Include options with implied volatility "
            "converged only if True, otherwise include all options regardless "
            "of convergence"
        ),
    ] = False,
) -> list[OptionPrice]:
    "List of selected option prices in the surface"
    return list(self.option_prices(select=select, index=index, converged=converged))

bs

bs(*, select=BEST, index=None, initial_vol=INITIAL_VOL)

Calculate Black-Scholes implied volatility for options in the surface. For some option prices, the implied volatility calculation may not converge, in this case the implied volatility is not calculated correctly and the option is marked as not converged.

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

Source code in quantflow/options/surface.py
def bs(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
) -> list[OptionPrice]:
    """Calculate Black-Scholes implied volatility for options
    in the surface.
    For some option prices, the implied volatility calculation may not converge,
    in this case the implied volatility is not
    calculated correctly and the option is marked as not converged.
    """
    self.reset_convergence()
    d = self.as_array(
        select=select,
        index=index,
        initial_vol=initial_vol,
    )
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        result = implied_black_volatility(
            k=d.log_strike,
            price=d.price,
            ttm=d.ttm,
            initial_sigma=d.iv,
            call_put=d.call_put,
        )
    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

calc_bs_prices

calc_bs_prices(*, select=BEST, index=None)

calculate Black-Scholes prices for all options in the surface

It uses options with a converged implied volatility calculation only, otherwise the price calculation won't be correct.

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

Source code in quantflow/options/surface.py
def calc_bs_prices(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
) -> FloatArray:
    """calculate Black-Scholes prices for all options in the surface

    It uses options with a converged implied volatility calculation only,
    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.iv, ttm=d.ttm, s=d.call_put)

options_df

options_df(
    *,
    select=BEST,
    index=None,
    initial_vol=INITIAL_VOL,
    converged=False
)

Time frame of Black-Scholes call input data

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

converged

Whether the calculation has converged

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def options_df(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
    converged: Annotated[
        bool, Doc("Whether the calculation has converged")
    ] = False,
) -> pd.DataFrame:
    """Time frame of Black-Scholes call input data"""
    data = self.option_prices(
        select=select,
        index=index,
        initial_vol=initial_vol,
        converged=converged,
    )
    return pd.DataFrame([d.info_dict() for d in data])

as_array

as_array(
    *,
    select=BEST,
    index=None,
    initial_vol=INITIAL_VOL,
    converged=False
)

Organize option prices in a numpy arrays for Black volatility and price calculation

It returns an OptionArrays instance, which contains the option prices and their corresponding log strikes, time to maturity and implied volatility in numpy arrays for efficient calculations.

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

converged

If True, include only options for which the calculation has converged

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def as_array(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
    converged: Annotated[
        bool,
        Doc(
            "If True, include only options for which the calculation has converged"
        ),
    ] = False,
) -> OptionArrays:
    """Organize option prices in a numpy arrays for Black volatility
    and price calculation

    It returns an [OptionArrays][quantflow.options.surface.OptionArrays] instance,
    which contains the option prices and their corresponding log strikes,
    time to maturity and implied volatility in numpy arrays
    for efficient calculations.
    """
    options = list(
        self.option_prices(
            select=select,
            index=index,
            initial_vol=initial_vol,
            converged=converged,
        )
    )
    log_strike = []
    ttm = []
    price = []
    vol = []
    call_put = []
    for option in options:
        log_strike.append(float(option.log_strike))
        price.append(float(option.price_in_forward_space))
        ttm.append(float(option.ttm))
        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),
        iv=np.array(vol),
        call_put=np.array(call_put),
    )

reset_convergence

reset_convergence()

Reset the convergence flag for all options in the surface

Source code in quantflow/options/surface.py
def reset_convergence(self) -> None:
    """Reset the convergence flag for all options in the surface"""
    for option in self.option_prices(select=OptionSelection.ALL):
        option.converged = False

disable_outliers

disable_outliers(
    *,
    bid_ask_spread_fraction=0.2,
    svi_residual_fraction=0.2,
    repeat=2
)

Disable outlier options across all maturities in the surface.

Calls VolCrossSection.disable_outliers on each maturity with the same parameters.

PARAMETER DESCRIPTION
bid_ask_spread_fraction

Maximum allowed bid/ask spread as a fraction of the mid implied volatility. A value of 0.2 means options with a spread greater than 20% of the mid vol are disabled.

TYPE: float DEFAULT: 0.2

svi_residual_fraction

Maximum allowed SVI residual as a fraction of the mid implied volatility. A value of 0.2 means options whose mid vol deviates from the SVI fit by more than 20% of their mid vol are disabled.

TYPE: float DEFAULT: 0.2

repeat

Number of times to repeat the outlier removal process

TYPE: int DEFAULT: 2

Source code in quantflow/options/surface.py
def disable_outliers(
    self,
    *,
    bid_ask_spread_fraction: Annotated[
        float,
        Doc(
            "Maximum allowed bid/ask spread as a fraction of the mid implied "
            "volatility. A value of 0.2 means options with a spread greater than "
            "20% of the mid vol are disabled."
        ),
    ] = 0.2,
    svi_residual_fraction: Annotated[
        float,
        Doc(
            "Maximum allowed SVI residual as a fraction of the mid implied "
            "volatility. A value of 0.2 means options whose mid vol deviates "
            "from the SVI fit by more than 20% of their mid vol are disabled."
        ),
    ] = 0.2,
    repeat: Annotated[
        int, Doc("Number of times to repeat the outlier removal process")
    ] = 2,
) -> Self:
    """Disable outlier options across all maturities in the surface.

    Calls
    [VolCrossSection.disable_outliers]
    [quantflow.options.surface.VolCrossSection.disable_outliers]
    on each maturity with the same parameters.
    """
    for maturity in self.maturities:
        maturity.disable_outliers(
            ttm=maturity.ttm(self.ref_date),
            bid_ask_spread_fraction=bid_ask_spread_fraction,
            svi_residual_fraction=svi_residual_fraction,
            repeat=repeat,
        )
    return self

plot

plot(*, index=None, select=BEST, **kwargs)

Plot the volatility surface

PARAMETER DESCRIPTION
index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

Source code in quantflow/options/surface.py
def plot(
    self,
    *,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    **kwargs: Any,
) -> Any:
    """Plot the volatility surface"""
    df = self.options_df(index=index, select=select, converged=True)
    return plot.plot_vol_surface(df, **kwargs)

plot3d

plot3d(
    *,
    select=BEST,
    index=None,
    dragmode="turntable",
    **kwargs
)

Plot the volatility surface

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

index

Index of the cross section to use, if None use all

TYPE: int | None DEFAULT: None

dragmode

Drag interaction mode for the 3D scene

TYPE: str DEFAULT: 'turntable'

Source code in quantflow/options/surface.py
def plot3d(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    index: Annotated[
        int | None, Doc("Index of the cross section to use, if None use all")
    ] = None,
    dragmode: Annotated[
        str, Doc("Drag interaction mode for the 3D scene")
    ] = "turntable",
    **kwargs: Any,
) -> Any:
    """Plot the volatility surface"""
    df = self.options_df(select=select, index=index, converged=True)
    return plot.plot_vol_surface_3d(df, dragmode=dragmode, **kwargs)

spot_price

spot_price()

Get the spot price if it exists

Source code in quantflow/options/surface.py
def spot_price(self) -> Decimal:
    """Get the spot price if it exists"""
    if self.spot is None:
        raise ValueError("No spot price provided")
    return self.spot.mid

forward

forward(maturity)

Calculate the implied forward for a given maturity

Source code in quantflow/options/surface.py
def forward(self, maturity: datetime) -> Decimal:
    """Calculate the implied forward for a given maturity"""
    ttm = self.day_counter.dcf(self.ref_date, maturity)
    df_quote = to_decimal(float(self.quote_curve.discount_factor(ttm)))
    df_asset = to_decimal(float(self.asset_curve.discount_factor(ttm)))
    forward_rate = self.spot_price() * df_asset / df_quote
    return self.clip_forward(forward_rate)

clip_forward

clip_forward(forward, rounding=ZERO)

Clip the forward price to the nearest tick size if tick_size_forwards is set

Source code in quantflow/options/surface.py
def clip_forward(
    self,
    forward: Decimal,
    rounding: Rounding = Rounding.ZERO,
) -> Decimal:
    """Clip the forward price to the nearest tick size if tick_size_forwards
    is set"""
    if self.tick_size_forwards:
        return round_to_step(forward, self.tick_size_forwards, rounding)
    return forward

quantflow.options.surface.VolCrossSection pydantic-model

Bases: BaseModel, Generic[S]

Represents a cross section of a volatility surface at a specific maturity.

Fields:

maturity pydantic-field

maturity

Maturity date of the cross section

forward pydantic-field

forward

Forward price of the underlying asset at the time of the cross section

strikes pydantic-field

strikes

Tuple of sorted strikes and their corresponding option prices

day_counter pydantic-field

day_counter = default_day_counter

Day counter for time to maturity calculations - by default it uses Act/Act

ttm

ttm(ref_date)

Time to maturity in years

Source code in quantflow/options/surface.py
def ttm(self, ref_date: datetime) -> float:
    """Time to maturity in years"""
    return self.day_counter.dcf(ref_date, self.maturity)

info_dict

info_dict(ref_date, spot, implied_forward)

Return a dictionary with information about the cross section

Source code in quantflow/options/surface.py
def info_dict(
    self,
    ref_date: datetime,
    spot: Decimal,
    implied_forward: Decimal,
) -> dict:
    """Return a dictionary with information about the cross section"""
    ttm = self.ttm(ref_date)
    return dict(
        maturity=self.maturity,
        ttm=ttm,
        forward=self.forward.mid,
        implied_forward=implied_forward,
        forward_basis=implied_forward - self.forward.mid,
        rate=Rate.from_number(float((implied_forward / spot).ln()) / ttm).rate,
        bid_ask_spread=self.forward.spread,
        basis=implied_forward - spot,
        open_interest=self.forward.open_interest,
        volume=self.forward.volume,
    )

option_prices

option_prices(
    ref_date,
    forward,
    *,
    select=BEST,
    initial_vol=INITIAL_VOL,
    converged=False
)

Iterator over option prices in the cross section

PARAMETER DESCRIPTION
ref_date

Reference date for time to maturity calculation

TYPE: datetime

forward

Forward price of the underlying asset

TYPE: Decimal

select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

converged

Whether the calculation has converged

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def option_prices(
    self,
    ref_date: Annotated[
        datetime, Doc("Reference date for time to maturity calculation")
    ],
    forward: Annotated[Decimal, Doc("Forward price of the underlying asset")],
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
    converged: Annotated[
        bool, Doc("Whether the calculation has converged")
    ] = False,
) -> Iterator[OptionPrice]:
    """Iterator over option prices in the cross section"""
    for s in self.strikes:
        yield from s.option_prices(
            forward,
            self.ttm(ref_date),
            select=select,
            initial_vol=initial_vol,
            converged=converged,
        )

securities

securities(*, select=ALL, converged=False)

Iterator over all securities in the cross section

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: ALL

converged

Include the forward and options with implied volatility converged only if True, otherwise include all securities regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def securities(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.ALL,
    converged: Annotated[
        bool,
        Doc(
            "Include the forward and options with implied volatility "
            "converged only if `True`, otherwise include all securities regardless "
            "of convergence"
        ),
    ] = False,
) -> Iterator[FwdPrice[S] | OptionPrices[S]]:
    """Iterator over all securities in the cross section"""
    yield self.forward
    yield from self.option_securities(select=select, converged=converged)

option_securities

option_securities(*, select=ALL, converged=False)

Iterator over all option securities in the cross section

PARAMETER DESCRIPTION
select

Option selection method

TYPE: OptionSelection DEFAULT: ALL

converged

Include the forward and options with implied volatility converged only if True, otherwise include all securities regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def option_securities(
    self,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.ALL,
    converged: Annotated[
        bool,
        Doc(
            "Include the forward and options with implied volatility "
            "converged only if `True`, otherwise include all securities regardless "
            "of convergence"
        ),
    ] = False,
) -> Iterator[OptionPrices[S]]:
    """Iterator over all option securities in the cross section"""
    for strike in self.strikes:
        yield from strike.securities(
            self.forward.mid,
            select=select,
            converged=converged,
        )

disable_outliers

disable_outliers(
    *,
    ttm,
    bid_ask_spread_fraction=0.2,
    svi_residual_fraction=0.2,
    repeat=2
)

Disable outlier options in the cross section by marking them as not converged.

Two passes are applied:

First pass: options where the bid/ask spread in implied vol space exceeds bid_ask_spread_fraction of the mid implied vol are disabled. For example, a value of 0.2 disables options where the spread is more than 20% of the mid vol. Options with a zero mid vol are also disabled.

Second pass: an SVI smile is fitted to the surviving options (mid implied vol vs log-strike). Options whose residual from the SVI fit exceeds svi_residual_fraction of their mid implied vol are disabled. This is repeated up to repeat times, refitting after each removal. The loop stops early if no outliers are found or fewer than 5 options remain.

PARAMETER DESCRIPTION
ttm

Time to maturity in years, used for SVI fitting

TYPE: float

bid_ask_spread_fraction

Maximum allowed bid/ask spread as a fraction of the mid implied volatility. A value of 0.2 means options with a spread greater than 20% of the mid vol are disabled.

TYPE: float DEFAULT: 0.2

svi_residual_fraction

Maximum allowed SVI residual as a fraction of the mid implied volatility. A value of 0.2 means options whose mid vol deviates from the SVI fit by more than 20% of their mid vol are disabled.

TYPE: float DEFAULT: 0.2

repeat

Number of times to repeat the outlier removal process

TYPE: int DEFAULT: 2

Source code in quantflow/options/surface.py
def disable_outliers(
    self,
    *,
    ttm: Annotated[float, Doc("Time to maturity in years, used for SVI fitting")],
    bid_ask_spread_fraction: Annotated[
        float,
        Doc(
            "Maximum allowed bid/ask spread as a fraction of the mid implied "
            "volatility. A value of 0.2 means options with a spread greater than "
            "20% of the mid vol are disabled."
        ),
    ] = 0.2,
    svi_residual_fraction: Annotated[
        float,
        Doc(
            "Maximum allowed SVI residual as a fraction of the mid implied "
            "volatility. A value of 0.2 means options whose mid vol deviates "
            "from the SVI fit by more than 20% of their mid vol are disabled."
        ),
    ] = 0.2,
    repeat: Annotated[
        int, Doc("Number of times to repeat the outlier removal process")
    ] = 2,
) -> None:
    """Disable outlier options in the cross section by marking them as not
    converged.

    Two passes are applied:

    First pass: options where the bid/ask spread in implied vol space exceeds
    `bid_ask_spread_fraction` of the mid implied vol are disabled.
    For example, a value of 0.2 disables options where the spread is more
    than 20% of the mid vol. Options with a zero mid vol are also disabled.

    Second pass: an [SVI][quantflow.options.svi.SVI] smile is fitted to the
    surviving options (mid implied vol vs log-strike). Options whose
    residual from the SVI fit exceeds `svi_residual_fraction` of their mid
    implied vol are disabled. This is repeated up to `repeat` times,
    refitting after each removal. The loop stops early if no outliers are
    found or fewer than 5 options remain.
    """
    options = list(self.option_securities(converged=True))
    # first remove options with high bid/offer spread
    for option in options:
        spread = option.iv_bid_ask_spread()
        mid = option.iv_mid()
        if mid > 0:
            if spread / mid > bid_ask_spread_fraction:
                option.disable()
        else:
            option.disable()
    # remove outliers based on residuals from an SVI smile fit
    forward = float(self.forward.mid)
    for _ in range(repeat):
        options = list(self.option_securities(converged=True))
        if len(options) < 5:
            break
        log_m = np.array([np.log(float(o.meta.strike) / forward) for o in options])
        iv_mid = np.array([o.iv_mid() for o in options])
        try:
            svi = SVI.fit(log_m, iv_mid, ttm)
        except Exception:
            break
        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):
            if residual > svi_residual_fraction:
                option.disable()
                found = True
        if not found:
            break

quantflow.options.surface.GenericVolSurfaceLoader pydantic-model

Bases: ForwardPricer[S]

Helper class to build a volatility surface from a list of securities

Use this class to add spot, forward and option securities with their prices and then call the surface method to build a VolSurface instance from the provided data.

Fields:

maturities pydantic-field

maturities

Dictionary of maturities and their corresponding cross section loaders

exclude_open_interest pydantic-field

exclude_open_interest = None

Exclude options with open interest at or below this value

exclude_volume pydantic-field

exclude_volume = None

Exclude options with volume at or below this value

asset pydantic-field

asset = ''

Name of the underlying asset

spot pydantic-field

spot = None

Spot price of the underlying asset

quote_curve pydantic-field

quote_curve

Discount curve for the quote

asset_curve pydantic-field

asset_curve

Discount curve for the asset

tick_size_forwards pydantic-field

tick_size_forwards = None

Tick size for rounding forward and spot prices - optional

tick_size_options pydantic-field

tick_size_options = None

Tick size for rounding option prices - optional

day_counter pydantic-field

day_counter = default_day_counter

Day counter for time to maturity calculations, by default it uses Act/Act

ref_date property

ref_date

Reference date for the volatility surface, taken as the earliest maturity or the provided ref_date if it's earlier

get_or_create_maturity

get_or_create_maturity(maturity)

Get or create a VolCrossSectionLoader for a given maturity

PARAMETER DESCRIPTION
maturity

Maturity date for the options

TYPE: datetime

Source code in quantflow/options/surface.py
def get_or_create_maturity(
    self,
    maturity: Annotated[datetime, Doc("Maturity date for the options")],
) -> VolCrossSectionLoader[S]:
    """Get or create a
    [VolCrossSectionLoader][quantflow.options.surface.VolCrossSectionLoader]
    for a given maturity"""
    if maturity not in self.maturities:
        self.maturities[maturity] = VolCrossSectionLoader(
            maturity=maturity,
            day_counter=self.day_counter,
        )
    return self.maturities[maturity]

add_spot

add_spot(
    security, bid, ask, open_interest=ZERO, volume=ZERO
)

Add a spot to the volatility surface loader

PARAMETER DESCRIPTION
security

Security for the spot price

TYPE: S

bid

Bid price for the spot

TYPE: Decimal

ask

Ask price for the spot

TYPE: Decimal

open_interest

Open interest for the spot

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the spot

TYPE: Decimal DEFAULT: ZERO

Source code in quantflow/options/surface.py
def add_spot(
    self,
    security: Annotated[S, Doc("Security for the spot price")],
    bid: Annotated[Decimal, Doc("Bid price for the spot")],
    ask: Annotated[Decimal, Doc("Ask price for the spot")],
    open_interest: Annotated[Decimal, Doc("Open interest for the spot")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the spot")] = ZERO,
) -> None:
    """Add a spot to the volatility surface loader"""
    if security.vol_surface_type() != VolSecurityType.SPOT:
        raise ValueError("Security is not a spot")
    self.spot = SpotPrice(
        security=security,
        bid=normalize_decimal(bid),
        ask=normalize_decimal(ask),
        open_interest=normalize_decimal(open_interest),
        volume=normalize_decimal(volume),
    )

add_forward

add_forward(
    security,
    maturity,
    bid,
    ask,
    open_interest=ZERO,
    volume=ZERO,
)

Add a forward to the volatility surface loader

PARAMETER DESCRIPTION
security

Security for the forward price

TYPE: S

maturity

Maturity date for the forward price

TYPE: datetime

bid

Bid price for the forward

TYPE: Decimal

ask

Ask price for the forward

TYPE: Decimal

open_interest

Open interest for the forward

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the forward

TYPE: Decimal DEFAULT: ZERO

Source code in quantflow/options/surface.py
def add_forward(
    self,
    security: Annotated[S, Doc("Security for the forward price")],
    maturity: Annotated[datetime, Doc("Maturity date for the forward price")],
    bid: Annotated[Decimal, Doc("Bid price for the forward")],
    ask: Annotated[Decimal, Doc("Ask price for the forward")],
    open_interest: Annotated[Decimal, Doc("Open interest for the forward")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the forward")] = ZERO,
) -> None:
    """Add a forward to the volatility surface loader"""
    if security.vol_surface_type() != VolSecurityType.FORWARD:
        raise ValueError("Security is not a forward")
    self.get_or_create_maturity(maturity=maturity).forward = FwdPrice(
        security=security,
        bid=normalize_decimal(bid),
        ask=normalize_decimal(ask),
        maturity=maturity,
        open_interest=normalize_decimal(open_interest),
        volume=normalize_decimal(volume),
    )

add_option

add_option(
    security,
    strike,
    maturity,
    option_type,
    bid,
    ask,
    open_interest=ZERO,
    volume=ZERO,
    inverse=True,
)

Add an option to the volatility surface loader

PARAMETER DESCRIPTION
security

Security for the option

TYPE: S

strike

Strike price for the option

TYPE: Decimal

maturity

Maturity date for the option

TYPE: datetime

option_type

Type of the option (call or put)

TYPE: OptionType

bid

Bid price for the option

TYPE: Decimal

ask

Ask price for the option

TYPE: Decimal

open_interest

Open interest for the option

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the option

TYPE: Decimal DEFAULT: ZERO

inverse

Whether the option is an inverse option

TYPE: bool DEFAULT: True

Source code in quantflow/options/surface.py
def add_option(
    self,
    security: Annotated[S, Doc("Security for the option")],
    strike: Annotated[Decimal, Doc("Strike price for the option")],
    maturity: Annotated[datetime, Doc("Maturity date for the option")],
    option_type: Annotated[OptionType, Doc("Type of the option (call or put)")],
    bid: Annotated[Decimal, Doc("Bid price for the option")],
    ask: Annotated[Decimal, Doc("Ask price for the option")],
    open_interest: Annotated[Decimal, Doc("Open interest for the option")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the option")] = ZERO,
    inverse: Annotated[bool, Doc("Whether the option is an inverse option")] = True,
) -> None:
    """Add an option to the volatility surface loader"""
    if security.vol_surface_type() != VolSecurityType.OPTION:
        raise ValueError("Security is not an option")
    if self.exclude_volume is not None and volume <= self.exclude_volume:
        return
    if (
        self.exclude_open_interest is not None
        and open_interest <= self.exclude_open_interest
    ):
        return
    self.get_or_create_maturity(maturity=maturity).add_option(
        security=security,
        strike=strike,
        option_type=option_type,
        bid=bid,
        ask=ask,
        open_interest=open_interest,
        volume=volume,
        inverse=inverse,
    )

surface

surface()

Build a volatility surface from the provided data

Source code in quantflow/options/surface.py
def surface(self) -> VolSurface[S]:
    """Build a volatility surface from the provided data"""
    maturities = []
    spot = self.spot
    if spot is None:
        raise ValueError("No spot price provided")
    for maturity in sorted(self.maturities):
        loader = self.maturities[maturity]
        forward = loader.forward
        if forward is None:
            implied_forward_price = self.forward(maturity)
            forward = spot._implied_forward(maturity, implied_forward_price)
        if section := loader._cross_section(forward):
            maturities.append(section)
    return VolSurface(
        asset=self.asset,
        spot=self.spot,
        maturities=tuple(maturities),
        day_counter=self.day_counter,
        quote_curve=self.quote_curve.model_copy(),
        asset_curve=self.asset_curve.model_copy(),
        tick_size_forwards=self.tick_size_forwards,
        tick_size_options=self.tick_size_options,
    )

calibrate_spot

calibrate_spot(*, max_ttm=1.0 / 52, max_pairs=10)

Calibrate the spot price from short-dated put-call parity.

For short-dated options where discount factors are approximately 1, put-call parity simplifies to C - P = S - K, so S = C - P + K. This method computes the median implied spot across all put-call pairs with time to maturity at or below max_ttm and updates the spot price.

Returns the implied spot, or None if no maturities fall within max_ttm.

PARAMETER DESCRIPTION
max_ttm

Maximum time to maturity (in years) for maturities used to imply the spot price. Default is 1/52 (one week).

TYPE: float DEFAULT: 1.0 / 52

max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def calibrate_spot(
    self,
    *,
    max_ttm: Annotated[
        float,
        Doc(
            "Maximum time to maturity (in years) for maturities used to imply "
            "the spot price. Default is 1/52 (one week)."
        ),
    ] = 1.0
    / 52,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> Decimal | None:
    """Calibrate the spot price from short-dated put-call parity.

    For short-dated options where discount factors are approximately 1,
    put-call parity simplifies to C - P = S - K, so S = C - P + K.
    This method computes the median implied spot across all put-call pairs
    with time to maturity at or below max_ttm and updates the spot price.

    Returns the implied spot, or None if no maturities fall within max_ttm.
    """
    spot = self.spot
    if spot is None:
        raise ValueError("No spot price provided")
    ref_date = self.ref_date
    implied_spots: list[float] = []
    for maturity in sorted(self.maturities):
        ttm = self.day_counter.dcf(ref_date, maturity)
        if ttm <= 0:
            continue
        if ttm > max_ttm:
            break
        parities = self.maturities[maturity].put_call_parities(
            ONE, ref_date=ref_date, max_pairs=max_pairs
        )
        for p in parities.parities:
            implied_spots.append(float(p.mid + p.strike))
    if not implied_spots:
        return None
    implied_spot = self.clip_forward(to_decimal(float(np.median(implied_spots))))
    self.spot = SpotPrice(
        security=spot.security,
        bid=implied_spot,
        ask=implied_spot,
    )
    return implied_spot

calibrate_curves

calibrate_curves(
    *, quote_curve=None, asset_curve=None, max_pairs=10
)

Calibrate the quote and/or asset discount curves from option prices.

Three modes are supported:

Both curves: pass a curve type or instance for both curves. A single OLS regression per maturity identifies \(D_q\) and \(D_a\) simultaneously.

Asset only: pass a curve type or instance for asset_curve, leave quote_curve as None. The existing quote_curve is treated as known and \(D_a\) is solved analytically.

Quote only: pass a curve type or instance for quote_curve, leave asset_curve as None. The existing asset_curve is treated as known and \(D_q\) is solved analytically.

PARAMETER DESCRIPTION
quote_curve

YieldCurve type or instance to fit the quote currency discount curve \(D_q\) from option prices. When None the current quote_curve is unchanged.

TYPE: type[YieldCurve] | YieldCurve | None DEFAULT: None

asset_curve

YieldCurve type or instance to fit the asset discount curve \(D_a\) from option prices. When None the current asset_curve is unchanged.

TYPE: type[YieldCurve] | YieldCurve | None DEFAULT: None

max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def calibrate_curves(
    self,
    *,
    quote_curve: Annotated[
        type[YieldCurve] | YieldCurve | None,
        Doc(
            "YieldCurve type or instance to fit the quote currency discount "
            "curve $D_q$ from option prices. "
            "When None the current quote_curve is unchanged."
        ),
    ] = None,
    asset_curve: Annotated[
        type[YieldCurve] | YieldCurve | None,
        Doc(
            "YieldCurve type or instance to fit the asset discount curve $D_a$ "
            "from option prices. "
            "When None the current asset_curve is unchanged."
        ),
    ] = None,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> None:
    """Calibrate the quote and/or asset discount curves from option prices.

    Three modes are supported:

    Both curves: pass a curve type or instance for both curves.
    A single OLS regression per maturity identifies $D_q$ and $D_a$ simultaneously.

    Asset only: pass a curve type or instance for `asset_curve`, leave
    `quote_curve` as None.
    The existing `quote_curve` is treated as known and $D_a$ is solved analytically.

    Quote only: pass a curve type or instance for `quote_curve`, leave
    `asset_curve` as None.
    The existing `asset_curve` is treated as known and $D_q$ is solved analytically.
    """
    ttm, cp, strikes = self.collect_put_call_parities(max_pairs=max_pairs)
    asset_curve_input = (
        self._curve_calibrator(asset_curve) if asset_curve else self.asset_curve
    )
    quote_curve_input = (
        self._curve_calibrator(quote_curve) if quote_curve else self.quote_curve
    )
    calibration = OptionsDiscountingCalibration(
        asset_curve=asset_curve_input,
        quote_curve=quote_curve_input,
        ttm=ttm,
        cp=cp,
        strikes=strikes,
    )
    calibrated_asset_curve, calibrated_quote_curve = calibration.calibrate()
    self.asset_curve = cast(AnyYieldCurve, calibrated_asset_curve)
    self.quote_curve = cast(AnyYieldCurve, calibrated_quote_curve)

collect_put_call_parities

collect_put_call_parities(*, max_pairs=10)

Collect per-maturity continuously compounded rates from put-call parity.

PARAMETER DESCRIPTION
max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def collect_put_call_parities(
    self,
    *,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> tuple[FloatArray, FloatArray, FloatArray]:
    """Collect per-maturity continuously compounded rates from put-call parity."""
    if not self.spot or self.spot.mid == ZERO:
        raise ValueError("No spot price provided")
    spot = self.spot.mid
    ttms: list[FloatArray] = []
    cp: list[FloatArray] = []
    strikes: list[FloatArray] = []
    ref_date = self.ref_date
    for maturity, section in sorted(self.maturities.items()):
        ttm = self.day_counter.dcf(ref_date, maturity)
        if ttm <= 0:
            continue
        parities = section.put_call_parities(
            spot,
            ref_date=ref_date,
            max_pairs=max_pairs,
        )
        regressand = parities.regressand()
        if not regressand.size:
            continue
        ttms.append(np.full(regressand.shape, ttm, dtype=float))
        cp.append(regressand)
        strikes.append(parities.regressor())
    if not cp:
        raise ValueError("No put-call parity pairs available")
    return (
        np.concatenate(ttms),
        np.concatenate(cp),
        np.concatenate(strikes),
    )

implied_forward_term_structure

implied_forward_term_structure(*, max_pairs=10)

Return per-maturity implied forwards from put-call parity.

For each maturity, fits asset and quote discount factors from the most liquid put-call pairs and returns the implied forward spot * Da / Dq.

Returns a list of (maturity, ttm, forward) tuples, one per maturity for which a valid fit is available.

PARAMETER DESCRIPTION
max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def implied_forward_term_structure(
    self,
    *,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> list[tuple[datetime, float, float]]:
    """Return per-maturity implied forwards from put-call parity.

    For each maturity, fits asset and quote discount factors from the most
    liquid put-call pairs and returns the implied forward `spot * Da / Dq`.

    Returns a list of `(maturity, ttm, forward)` tuples, one per maturity
    for which a valid fit is available.
    """
    if not self.spot or self.spot.mid == ZERO:
        raise ValueError("No spot price provided")
    spot = self.spot.mid
    ref_date = self.ref_date
    result = []
    for maturity, section in sorted(self.maturities.items()):
        ttm = self.day_counter.dcf(ref_date, maturity)
        if ttm <= 0:
            continue
        parities = section.put_call_parities(
            spot, ref_date=ref_date, max_pairs=max_pairs
        )
        forward = parities.implied_forward()
        if forward is not None:
            result.append((maturity, ttm, forward))
    return result

spot_price

spot_price()

Get the spot price if it exists

Source code in quantflow/options/surface.py
def spot_price(self) -> Decimal:
    """Get the spot price if it exists"""
    if self.spot is None:
        raise ValueError("No spot price provided")
    return self.spot.mid

forward

forward(maturity)

Calculate the implied forward for a given maturity

Source code in quantflow/options/surface.py
def forward(self, maturity: datetime) -> Decimal:
    """Calculate the implied forward for a given maturity"""
    ttm = self.day_counter.dcf(self.ref_date, maturity)
    df_quote = to_decimal(float(self.quote_curve.discount_factor(ttm)))
    df_asset = to_decimal(float(self.asset_curve.discount_factor(ttm)))
    forward_rate = self.spot_price() * df_asset / df_quote
    return self.clip_forward(forward_rate)

clip_forward

clip_forward(forward, rounding=ZERO)

Clip the forward price to the nearest tick size if tick_size_forwards is set

Source code in quantflow/options/surface.py
def clip_forward(
    self,
    forward: Decimal,
    rounding: Rounding = Rounding.ZERO,
) -> Decimal:
    """Clip the forward price to the nearest tick size if tick_size_forwards
    is set"""
    if self.tick_size_forwards:
        return round_to_step(forward, self.tick_size_forwards, rounding)
    return forward

quantflow.options.surface.VolSurfaceLoader pydantic-model

Bases: GenericVolSurfaceLoader[DefaultVolSecurity]

Helper class to build a volatility surface from a list of securities

Use this class to add spot, forward and option securities with their prices and then call the surface method to build a VolSurface instance from the provided data.

Fields:

asset pydantic-field

asset = ''

Name of the underlying asset

spot pydantic-field

spot = None

Spot price of the underlying asset

quote_curve pydantic-field

quote_curve

Discount curve for the quote

asset_curve pydantic-field

asset_curve

Discount curve for the asset

tick_size_forwards pydantic-field

tick_size_forwards = None

Tick size for rounding forward and spot prices - optional

tick_size_options pydantic-field

tick_size_options = None

Tick size for rounding option prices - optional

day_counter pydantic-field

day_counter = default_day_counter

Day counter for time to maturity calculations, by default it uses Act/Act

ref_date property

ref_date

Reference date for the volatility surface, taken as the earliest maturity or the provided ref_date if it's earlier

maturities pydantic-field

maturities

Dictionary of maturities and their corresponding cross section loaders

exclude_open_interest pydantic-field

exclude_open_interest = None

Exclude options with open interest at or below this value

exclude_volume pydantic-field

exclude_volume = None

Exclude options with volume at or below this value

add

add(input)

Add a volatility security input to the loader

PARAMETER DESCRIPTION
input

Volatility surface input data

TYPE: VolSurfaceInput

Source code in quantflow/options/surface.py
def add(
    self, input: Annotated[VolSurfaceInput, Doc("Volatility surface input data")]
) -> None:
    """Add a volatility security input to the loader"""
    if isinstance(input, SpotInput):
        self.add_spot(
            DefaultVolSecurity.spot(),
            bid=input.bid,
            ask=input.ask,
            open_interest=input.open_interest,
            volume=input.volume,
        )
    elif isinstance(input, ForwardInput):
        self.add_forward(
            DefaultVolSecurity.forward(),
            maturity=input.maturity,
            bid=input.bid,
            ask=input.ask,
            open_interest=input.open_interest,
            volume=input.volume,
        )
    elif isinstance(input, OptionInput):
        self.add_option(
            DefaultVolSecurity.option(),
            strike=input.strike,
            option_type=input.option_type,
            maturity=input.maturity,
            bid=input.bid,
            ask=input.ask,
            open_interest=input.open_interest,
            volume=input.volume,
            inverse=input.inverse,
        )
    else:
        raise ValueError(f"Unknown input type {type(input)}")

spot_price

spot_price()

Get the spot price if it exists

Source code in quantflow/options/surface.py
def spot_price(self) -> Decimal:
    """Get the spot price if it exists"""
    if self.spot is None:
        raise ValueError("No spot price provided")
    return self.spot.mid

forward

forward(maturity)

Calculate the implied forward for a given maturity

Source code in quantflow/options/surface.py
def forward(self, maturity: datetime) -> Decimal:
    """Calculate the implied forward for a given maturity"""
    ttm = self.day_counter.dcf(self.ref_date, maturity)
    df_quote = to_decimal(float(self.quote_curve.discount_factor(ttm)))
    df_asset = to_decimal(float(self.asset_curve.discount_factor(ttm)))
    forward_rate = self.spot_price() * df_asset / df_quote
    return self.clip_forward(forward_rate)

clip_forward

clip_forward(forward, rounding=ZERO)

Clip the forward price to the nearest tick size if tick_size_forwards is set

Source code in quantflow/options/surface.py
def clip_forward(
    self,
    forward: Decimal,
    rounding: Rounding = Rounding.ZERO,
) -> Decimal:
    """Clip the forward price to the nearest tick size if tick_size_forwards
    is set"""
    if self.tick_size_forwards:
        return round_to_step(forward, self.tick_size_forwards, rounding)
    return forward

get_or_create_maturity

get_or_create_maturity(maturity)

Get or create a VolCrossSectionLoader for a given maturity

PARAMETER DESCRIPTION
maturity

Maturity date for the options

TYPE: datetime

Source code in quantflow/options/surface.py
def get_or_create_maturity(
    self,
    maturity: Annotated[datetime, Doc("Maturity date for the options")],
) -> VolCrossSectionLoader[S]:
    """Get or create a
    [VolCrossSectionLoader][quantflow.options.surface.VolCrossSectionLoader]
    for a given maturity"""
    if maturity not in self.maturities:
        self.maturities[maturity] = VolCrossSectionLoader(
            maturity=maturity,
            day_counter=self.day_counter,
        )
    return self.maturities[maturity]

add_spot

add_spot(
    security, bid, ask, open_interest=ZERO, volume=ZERO
)

Add a spot to the volatility surface loader

PARAMETER DESCRIPTION
security

Security for the spot price

TYPE: S

bid

Bid price for the spot

TYPE: Decimal

ask

Ask price for the spot

TYPE: Decimal

open_interest

Open interest for the spot

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the spot

TYPE: Decimal DEFAULT: ZERO

Source code in quantflow/options/surface.py
def add_spot(
    self,
    security: Annotated[S, Doc("Security for the spot price")],
    bid: Annotated[Decimal, Doc("Bid price for the spot")],
    ask: Annotated[Decimal, Doc("Ask price for the spot")],
    open_interest: Annotated[Decimal, Doc("Open interest for the spot")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the spot")] = ZERO,
) -> None:
    """Add a spot to the volatility surface loader"""
    if security.vol_surface_type() != VolSecurityType.SPOT:
        raise ValueError("Security is not a spot")
    self.spot = SpotPrice(
        security=security,
        bid=normalize_decimal(bid),
        ask=normalize_decimal(ask),
        open_interest=normalize_decimal(open_interest),
        volume=normalize_decimal(volume),
    )

add_forward

add_forward(
    security,
    maturity,
    bid,
    ask,
    open_interest=ZERO,
    volume=ZERO,
)

Add a forward to the volatility surface loader

PARAMETER DESCRIPTION
security

Security for the forward price

TYPE: S

maturity

Maturity date for the forward price

TYPE: datetime

bid

Bid price for the forward

TYPE: Decimal

ask

Ask price for the forward

TYPE: Decimal

open_interest

Open interest for the forward

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the forward

TYPE: Decimal DEFAULT: ZERO

Source code in quantflow/options/surface.py
def add_forward(
    self,
    security: Annotated[S, Doc("Security for the forward price")],
    maturity: Annotated[datetime, Doc("Maturity date for the forward price")],
    bid: Annotated[Decimal, Doc("Bid price for the forward")],
    ask: Annotated[Decimal, Doc("Ask price for the forward")],
    open_interest: Annotated[Decimal, Doc("Open interest for the forward")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the forward")] = ZERO,
) -> None:
    """Add a forward to the volatility surface loader"""
    if security.vol_surface_type() != VolSecurityType.FORWARD:
        raise ValueError("Security is not a forward")
    self.get_or_create_maturity(maturity=maturity).forward = FwdPrice(
        security=security,
        bid=normalize_decimal(bid),
        ask=normalize_decimal(ask),
        maturity=maturity,
        open_interest=normalize_decimal(open_interest),
        volume=normalize_decimal(volume),
    )

add_option

add_option(
    security,
    strike,
    maturity,
    option_type,
    bid,
    ask,
    open_interest=ZERO,
    volume=ZERO,
    inverse=True,
)

Add an option to the volatility surface loader

PARAMETER DESCRIPTION
security

Security for the option

TYPE: S

strike

Strike price for the option

TYPE: Decimal

maturity

Maturity date for the option

TYPE: datetime

option_type

Type of the option (call or put)

TYPE: OptionType

bid

Bid price for the option

TYPE: Decimal

ask

Ask price for the option

TYPE: Decimal

open_interest

Open interest for the option

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the option

TYPE: Decimal DEFAULT: ZERO

inverse

Whether the option is an inverse option

TYPE: bool DEFAULT: True

Source code in quantflow/options/surface.py
def add_option(
    self,
    security: Annotated[S, Doc("Security for the option")],
    strike: Annotated[Decimal, Doc("Strike price for the option")],
    maturity: Annotated[datetime, Doc("Maturity date for the option")],
    option_type: Annotated[OptionType, Doc("Type of the option (call or put)")],
    bid: Annotated[Decimal, Doc("Bid price for the option")],
    ask: Annotated[Decimal, Doc("Ask price for the option")],
    open_interest: Annotated[Decimal, Doc("Open interest for the option")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the option")] = ZERO,
    inverse: Annotated[bool, Doc("Whether the option is an inverse option")] = True,
) -> None:
    """Add an option to the volatility surface loader"""
    if security.vol_surface_type() != VolSecurityType.OPTION:
        raise ValueError("Security is not an option")
    if self.exclude_volume is not None and volume <= self.exclude_volume:
        return
    if (
        self.exclude_open_interest is not None
        and open_interest <= self.exclude_open_interest
    ):
        return
    self.get_or_create_maturity(maturity=maturity).add_option(
        security=security,
        strike=strike,
        option_type=option_type,
        bid=bid,
        ask=ask,
        open_interest=open_interest,
        volume=volume,
        inverse=inverse,
    )

surface

surface()

Build a volatility surface from the provided data

Source code in quantflow/options/surface.py
def surface(self) -> VolSurface[S]:
    """Build a volatility surface from the provided data"""
    maturities = []
    spot = self.spot
    if spot is None:
        raise ValueError("No spot price provided")
    for maturity in sorted(self.maturities):
        loader = self.maturities[maturity]
        forward = loader.forward
        if forward is None:
            implied_forward_price = self.forward(maturity)
            forward = spot._implied_forward(maturity, implied_forward_price)
        if section := loader._cross_section(forward):
            maturities.append(section)
    return VolSurface(
        asset=self.asset,
        spot=self.spot,
        maturities=tuple(maturities),
        day_counter=self.day_counter,
        quote_curve=self.quote_curve.model_copy(),
        asset_curve=self.asset_curve.model_copy(),
        tick_size_forwards=self.tick_size_forwards,
        tick_size_options=self.tick_size_options,
    )

calibrate_spot

calibrate_spot(*, max_ttm=1.0 / 52, max_pairs=10)

Calibrate the spot price from short-dated put-call parity.

For short-dated options where discount factors are approximately 1, put-call parity simplifies to C - P = S - K, so S = C - P + K. This method computes the median implied spot across all put-call pairs with time to maturity at or below max_ttm and updates the spot price.

Returns the implied spot, or None if no maturities fall within max_ttm.

PARAMETER DESCRIPTION
max_ttm

Maximum time to maturity (in years) for maturities used to imply the spot price. Default is 1/52 (one week).

TYPE: float DEFAULT: 1.0 / 52

max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def calibrate_spot(
    self,
    *,
    max_ttm: Annotated[
        float,
        Doc(
            "Maximum time to maturity (in years) for maturities used to imply "
            "the spot price. Default is 1/52 (one week)."
        ),
    ] = 1.0
    / 52,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> Decimal | None:
    """Calibrate the spot price from short-dated put-call parity.

    For short-dated options where discount factors are approximately 1,
    put-call parity simplifies to C - P = S - K, so S = C - P + K.
    This method computes the median implied spot across all put-call pairs
    with time to maturity at or below max_ttm and updates the spot price.

    Returns the implied spot, or None if no maturities fall within max_ttm.
    """
    spot = self.spot
    if spot is None:
        raise ValueError("No spot price provided")
    ref_date = self.ref_date
    implied_spots: list[float] = []
    for maturity in sorted(self.maturities):
        ttm = self.day_counter.dcf(ref_date, maturity)
        if ttm <= 0:
            continue
        if ttm > max_ttm:
            break
        parities = self.maturities[maturity].put_call_parities(
            ONE, ref_date=ref_date, max_pairs=max_pairs
        )
        for p in parities.parities:
            implied_spots.append(float(p.mid + p.strike))
    if not implied_spots:
        return None
    implied_spot = self.clip_forward(to_decimal(float(np.median(implied_spots))))
    self.spot = SpotPrice(
        security=spot.security,
        bid=implied_spot,
        ask=implied_spot,
    )
    return implied_spot

calibrate_curves

calibrate_curves(
    *, quote_curve=None, asset_curve=None, max_pairs=10
)

Calibrate the quote and/or asset discount curves from option prices.

Three modes are supported:

Both curves: pass a curve type or instance for both curves. A single OLS regression per maturity identifies \(D_q\) and \(D_a\) simultaneously.

Asset only: pass a curve type or instance for asset_curve, leave quote_curve as None. The existing quote_curve is treated as known and \(D_a\) is solved analytically.

Quote only: pass a curve type or instance for quote_curve, leave asset_curve as None. The existing asset_curve is treated as known and \(D_q\) is solved analytically.

PARAMETER DESCRIPTION
quote_curve

YieldCurve type or instance to fit the quote currency discount curve \(D_q\) from option prices. When None the current quote_curve is unchanged.

TYPE: type[YieldCurve] | YieldCurve | None DEFAULT: None

asset_curve

YieldCurve type or instance to fit the asset discount curve \(D_a\) from option prices. When None the current asset_curve is unchanged.

TYPE: type[YieldCurve] | YieldCurve | None DEFAULT: None

max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def calibrate_curves(
    self,
    *,
    quote_curve: Annotated[
        type[YieldCurve] | YieldCurve | None,
        Doc(
            "YieldCurve type or instance to fit the quote currency discount "
            "curve $D_q$ from option prices. "
            "When None the current quote_curve is unchanged."
        ),
    ] = None,
    asset_curve: Annotated[
        type[YieldCurve] | YieldCurve | None,
        Doc(
            "YieldCurve type or instance to fit the asset discount curve $D_a$ "
            "from option prices. "
            "When None the current asset_curve is unchanged."
        ),
    ] = None,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> None:
    """Calibrate the quote and/or asset discount curves from option prices.

    Three modes are supported:

    Both curves: pass a curve type or instance for both curves.
    A single OLS regression per maturity identifies $D_q$ and $D_a$ simultaneously.

    Asset only: pass a curve type or instance for `asset_curve`, leave
    `quote_curve` as None.
    The existing `quote_curve` is treated as known and $D_a$ is solved analytically.

    Quote only: pass a curve type or instance for `quote_curve`, leave
    `asset_curve` as None.
    The existing `asset_curve` is treated as known and $D_q$ is solved analytically.
    """
    ttm, cp, strikes = self.collect_put_call_parities(max_pairs=max_pairs)
    asset_curve_input = (
        self._curve_calibrator(asset_curve) if asset_curve else self.asset_curve
    )
    quote_curve_input = (
        self._curve_calibrator(quote_curve) if quote_curve else self.quote_curve
    )
    calibration = OptionsDiscountingCalibration(
        asset_curve=asset_curve_input,
        quote_curve=quote_curve_input,
        ttm=ttm,
        cp=cp,
        strikes=strikes,
    )
    calibrated_asset_curve, calibrated_quote_curve = calibration.calibrate()
    self.asset_curve = cast(AnyYieldCurve, calibrated_asset_curve)
    self.quote_curve = cast(AnyYieldCurve, calibrated_quote_curve)

collect_put_call_parities

collect_put_call_parities(*, max_pairs=10)

Collect per-maturity continuously compounded rates from put-call parity.

PARAMETER DESCRIPTION
max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def collect_put_call_parities(
    self,
    *,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> tuple[FloatArray, FloatArray, FloatArray]:
    """Collect per-maturity continuously compounded rates from put-call parity."""
    if not self.spot or self.spot.mid == ZERO:
        raise ValueError("No spot price provided")
    spot = self.spot.mid
    ttms: list[FloatArray] = []
    cp: list[FloatArray] = []
    strikes: list[FloatArray] = []
    ref_date = self.ref_date
    for maturity, section in sorted(self.maturities.items()):
        ttm = self.day_counter.dcf(ref_date, maturity)
        if ttm <= 0:
            continue
        parities = section.put_call_parities(
            spot,
            ref_date=ref_date,
            max_pairs=max_pairs,
        )
        regressand = parities.regressand()
        if not regressand.size:
            continue
        ttms.append(np.full(regressand.shape, ttm, dtype=float))
        cp.append(regressand)
        strikes.append(parities.regressor())
    if not cp:
        raise ValueError("No put-call parity pairs available")
    return (
        np.concatenate(ttms),
        np.concatenate(cp),
        np.concatenate(strikes),
    )

implied_forward_term_structure

implied_forward_term_structure(*, max_pairs=10)

Return per-maturity implied forwards from put-call parity.

For each maturity, fits asset and quote discount factors from the most liquid put-call pairs and returns the implied forward spot * Da / Dq.

Returns a list of (maturity, ttm, forward) tuples, one per maturity for which a valid fit is available.

PARAMETER DESCRIPTION
max_pairs

Maximum number of put-call pairs to use per maturity

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def implied_forward_term_structure(
    self,
    *,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to use per maturity")
    ] = 10,
) -> list[tuple[datetime, float, float]]:
    """Return per-maturity implied forwards from put-call parity.

    For each maturity, fits asset and quote discount factors from the most
    liquid put-call pairs and returns the implied forward `spot * Da / Dq`.

    Returns a list of `(maturity, ttm, forward)` tuples, one per maturity
    for which a valid fit is available.
    """
    if not self.spot or self.spot.mid == ZERO:
        raise ValueError("No spot price provided")
    spot = self.spot.mid
    ref_date = self.ref_date
    result = []
    for maturity, section in sorted(self.maturities.items()):
        ttm = self.day_counter.dcf(ref_date, maturity)
        if ttm <= 0:
            continue
        parities = section.put_call_parities(
            spot, ref_date=ref_date, max_pairs=max_pairs
        )
        forward = parities.implied_forward()
        if forward is not None:
            result.append((maturity, ttm, forward))
    return result

quantflow.options.surface.VolCrossSectionLoader pydantic-model

Bases: BaseModel, Generic[S]

Fields:

maturity pydantic-field

maturity

Maturity date of the cross section

forward pydantic-field

forward = None

Forward price of the underlying asset at the time of the cross section

strikes pydantic-field

strikes

Dictionary of strikes and their corresponding option prices

day_counter pydantic-field

day_counter = default_day_counter

Day counter for time to maturity calculations - by default it uses Act/Act

add_option

add_option(
    security,
    strike,
    option_type,
    bid,
    ask,
    open_interest=ZERO,
    volume=ZERO,
    inverse=True,
)

Add an option to the cross section loader

PARAMETER DESCRIPTION
security

Security for the option

TYPE: S

strike

Strike price for the option

TYPE: Decimal

option_type

Type of the option (call or put)

TYPE: OptionType

bid

Bid price for the option

TYPE: Decimal

ask

Ask price for the option

TYPE: Decimal

open_interest

Open interest for the option

TYPE: Decimal DEFAULT: ZERO

volume

Volume for the option

TYPE: Decimal DEFAULT: ZERO

inverse

Whether the option is an inverse option

TYPE: bool DEFAULT: True

Source code in quantflow/options/surface.py
def add_option(
    self,
    security: Annotated[S, Doc("Security for the option")],
    strike: Annotated[Decimal, Doc("Strike price for the option")],
    option_type: Annotated[OptionType, Doc("Type of the option (call or put)")],
    bid: Annotated[Decimal, Doc("Bid price for the option")],
    ask: Annotated[Decimal, Doc("Ask price for the option")],
    open_interest: Annotated[Decimal, Doc("Open interest for the option")] = ZERO,
    volume: Annotated[Decimal, Doc("Volume for the option")] = ZERO,
    inverse: Annotated[bool, Doc("Whether the option is an inverse option")] = True,
) -> None:
    """Add an option to the cross section loader"""
    strike = normalize_decimal(strike)
    if strike not in self.strikes:
        self.strikes[strike] = Strike(strike=strike)
    meta = OptionMetadata(
        strike=strike,
        option_type=option_type,
        maturity=self.maturity,
        inverse=inverse,
    )
    option = OptionPrices(
        security=security,
        meta=meta,
        bid=OptionPrice(price=normalize_decimal(bid), meta=meta, side=Side.BID),
        ask=OptionPrice(price=normalize_decimal(ask), meta=meta, side=Side.ASK),
        open_interest=normalize_decimal(open_interest),
        volume=normalize_decimal(volume),
    )
    if option_type.is_call():
        self.strikes[strike].call = option
    else:
        self.strikes[strike].put = option

put_call_parities

put_call_parities(spot, *, ref_date=None, max_pairs=10)

Return a list of the most liquid PutCallParity from a cross-section loader.

Liquidity is determined by the bid-ask spread of the put-call parity price.

PARAMETER DESCRIPTION
spot

Spot price of the underlying asset

TYPE: Decimal

ref_date

Reference date for time to maturity calculation

TYPE: datetime | None DEFAULT: None

max_pairs

Maximum number of put-call pairs to consider

TYPE: int DEFAULT: 10

Source code in quantflow/options/surface.py
def put_call_parities(
    self,
    spot: Annotated[Decimal, Doc("Spot price of the underlying asset")],
    *,
    ref_date: Annotated[
        datetime | None, Doc("Reference date for time to maturity calculation")
    ] = None,
    max_pairs: Annotated[
        int, Doc("Maximum number of put-call pairs to consider")
    ] = 10,
) -> PutCallParities:
    """Return a list of the most liquid
    [PutCallParity][quantflow.options.parity.PutCallParities]
    from a cross-section loader.

    Liquidity is determined by the bid-ask spread of the put-call parity price.
    """
    ttm = self.day_counter.dcf(ref_date or utcnow(), self.maturity)
    parities = sorted(
        (
            p
            for sk in self.strikes.values()
            if (p := sk.put_call_parity()) is not None
        ),
        key=lambda p: p.spread,
    )[:max_pairs]
    return PutCallParities.from_parities(parities, spot, ttm)

Info Models

quantflow.options.surface.OptionInfo pydantic-model

Bases: BaseModel

Structured representation of an option price with all computed fields

Fields:

strike pydantic-field

strike

Strike price of the option

forward pydantic-field

forward

Forward price of the underlying at maturity

maturity pydantic-field

maturity

Maturity date of the option

log_strike pydantic-field

log_strike

Log strike, calculated as log(strike/forward)

moneyness pydantic-field

moneyness

Standardised moneyness, log(K/F) / sqrt(T)

ttm pydantic-field

ttm

Time to maturity in years

iv pydantic-field

iv

Black implied volatility

price pydantic-field

price

Option price as a fraction of the forward price

price_bp pydantic-field

price_bp

Option price in basis points

price_quote pydantic-field

price_quote

Option price in quote currency

option_type pydantic-field

option_type

Option type (call or put)

side pydantic-field

side

Market side (bid or ask)

open_interest pydantic-field

open_interest

Open interest

volume pydantic-field

volume

Volume traded

Bid/Ask Prices

quantflow.options.surface.SpotPrice pydantic-model

Bases: SecurityPrice[S]

Represents the spot bid/ask price of an underlying asset

Fields:

bid pydantic-field

bid

Bid price

ask pydantic-field

ask

Ask price

mid property

mid

Calculate the mid price by averaging the bid and ask prices

spread property

spread

Calculate the bid-ask spread

bp_spread property

bp_spread

Bid-ask spread in basis points, calculated as spread divided by mid price and multiplied by 10000

open_interest pydantic-field

open_interest = ZERO

Open interest of the security

volume pydantic-field

volume = ZERO

Volume of the security

security pydantic-field

security

The underlying security of the price

inputs

inputs()
Source code in quantflow/options/surface.py
def inputs(self) -> SpotInput:
    return SpotInput(
        bid=self.bid,
        ask=self.ask,
        open_interest=self.open_interest,
        volume=self.volume,
    )

is_valid

is_valid()

Check if the forward price is valid, which means that the bid and ask are positive and the bid is less than or equal to the ask

Source code in quantflow/options/surface.py
def is_valid(self) -> bool:
    """Check if the forward price is valid, which means that the bid and ask
    are positive and the bid is less than or equal to the ask"""
    return self.bid > ZERO and self.ask > ZERO and super().is_valid()

quantflow.options.surface.FwdPrice pydantic-model

Bases: SecurityPrice[S]

Represents the forward bid/ask price of an underlying asset at a specific maturity

Fields:

maturity pydantic-field

maturity

Maturity date of the forward price

bid pydantic-field

bid

Bid price

ask pydantic-field

ask

Ask price

mid property

mid

Calculate the mid price by averaging the bid and ask prices

spread property

spread

Calculate the bid-ask spread

bp_spread property

bp_spread

Bid-ask spread in basis points, calculated as spread divided by mid price and multiplied by 10000

open_interest pydantic-field

open_interest = ZERO

Open interest of the security

volume pydantic-field

volume = ZERO

Volume of the security

security pydantic-field

security

The underlying security of the price

inputs

inputs()
Source code in quantflow/options/surface.py
def inputs(self) -> ForwardInput:
    return ForwardInput(
        bid=self.bid,
        ask=self.ask,
        maturity=self.maturity,
        open_interest=self.open_interest,
        volume=self.volume,
    )

is_valid

is_valid()

Check if the forward price is valid, which means that the bid and ask are positive and the bid is less than or equal to the ask

Source code in quantflow/options/surface.py
def is_valid(self) -> bool:
    """Check if the forward price is valid, which means that the bid and ask
    are positive and the bid is less than or equal to the ask"""
    return self.bid > ZERO and self.ask > ZERO and super().is_valid()

quantflow.options.surface.Strike pydantic-model

Bases: BaseModel, Generic[S]

Option prices for a single strike

Fields:

strike pydantic-field

strike

Strike price of the options

call pydantic-field

call = None

Call option prices for the strike

put pydantic-field

put = None

Put option prices for the strike

put_call_parity

put_call_parity()

Return a [PutCallParity][quantflow.rates.calibrator.PutCallParity] for this strike, or None if either the call or the put are not available.

Source code in quantflow/options/surface.py
def put_call_parity(self) -> PutCallParity | None:
    """Return a [PutCallParity][quantflow.rates.calibrator.PutCallParity] for this
    strike, or None if either the call or the put are not available."""
    if self.call is None or self.put is None:
        return None
    return PutCallParity(
        strike=self.strike,
        call=self.call.price(),
        put=self.put.price(),
        inverse=self.call.meta.inverse,
    )

options_iter

options_iter(forward, ttm, *, select=ALL)

Iterator over option prices for the strike

It uses the select parameter to determine which options to include in the iteration. The forward price is used to determine the moneyness of the options when the best or otm selection method is used, in which case only the Out of the Money options are included in the iteration.

PARAMETER DESCRIPTION
forward

Forward price of the underlying asset

TYPE: Decimal

ttm

Time to maturity in years

TYPE: float

select

Option selection method

TYPE: OptionSelection DEFAULT: ALL

Source code in quantflow/options/surface.py
def options_iter(
    self,
    forward: Annotated[Decimal, Doc("Forward price of the underlying asset")],
    ttm: Annotated[float, Doc("Time to maturity in years")],
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.ALL,
) -> Iterator[OptionPrices[S]]:
    """Iterator over option prices for the strike

    It uses the `select` parameter to determine which options to include in
    the iteration. The forward price is used to determine the moneyness of
    the options when the `best` or `otm` selection method is used, in which
    case only the Out of the Money options are included in the iteration.
    """
    match select:
        case OptionSelection.OTM:
            if self.call and not self.call.is_in_the_money(forward):
                yield self.call
            elif self.put and not self.put.is_in_the_money(forward):
                yield self.put
        case OptionSelection.BEST:
            if self.call and not self.call.is_in_the_money(forward):
                yield self.call
            elif self.put and not self.put.is_in_the_money(forward):
                yield self.put
        case OptionSelection.CALL:
            if self.call:
                yield self.call
        case OptionSelection.PUT:
            if self.put:
                yield self.put
        case OptionSelection.ALL:
            if self.call:
                yield self.call
            if self.put:
                yield self.put

securities

securities(
    forward, ttm=0.0, *, select=ALL, converged=False
)

Iterator over option prices for the strike

PARAMETER DESCRIPTION
forward

Forward price of the underlying asset

TYPE: Decimal

ttm

Time to maturity in years

TYPE: float DEFAULT: 0.0

select

Option selection method

TYPE: OptionSelection DEFAULT: ALL

converged

Include options with implied volatility converged only if True, otherwise include all options regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def securities(
    self,
    forward: Annotated[Decimal, Doc("Forward price of the underlying asset")],
    ttm: Annotated[float, Doc("Time to maturity in years")] = 0.0,
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.ALL,
    converged: Annotated[
        bool,
        Doc(
            "Include options with implied volatility converged only if True, "
            "otherwise include all options regardless of convergence"
        ),
    ] = False,
) -> Iterator[OptionPrices[S]]:
    """Iterator over option prices for the strike"""
    for option in self.options_iter(forward, ttm, select=select):
        if not converged or option.converged:
            yield option

option_prices

option_prices(
    forward,
    ttm,
    *,
    select=BEST,
    initial_vol=INITIAL_VOL,
    converged=False
)
PARAMETER DESCRIPTION
forward

Forward price of the underlying asset

TYPE: Decimal

ttm

Time to maturity in years

TYPE: float

select

Option selection method

TYPE: OptionSelection DEFAULT: BEST

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

converged

Include options with implied volatility converged only if True, otherwise include all options regardless of convergence

TYPE: bool DEFAULT: False

Source code in quantflow/options/surface.py
def option_prices(
    self,
    forward: Annotated[Decimal, Doc("Forward price of the underlying asset")],
    ttm: Annotated[float, Doc("Time to maturity in years")],
    *,
    select: Annotated[
        OptionSelection, Doc("Option selection method")
    ] = OptionSelection.BEST,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
    converged: Annotated[
        bool,
        Doc(
            "Include options with implied volatility converged only if True, "
            "otherwise include all options regardless of convergence"
        ),
    ] = False,
) -> Iterator[OptionPrice]:
    for option in self.options_iter(forward, ttm, select=select):
        if not converged or option.converged:
            yield from option.prices(
                forward,
                ttm,
                initial_vol=initial_vol,
            )

quantflow.options.surface.OptionArrays

Bases: NamedTuple

Represents the option data in array form for efficient calculations via vectorized operations

options instance-attribute

options

List of option prices corresponding to the arrays below

log_strike instance-attribute

log_strike

The log strike of the options, calculated as log(strike/forward)

price instance-attribute

price

The option prices

ttm instance-attribute

ttm

Time to maturity of the options

iv instance-attribute

iv

Implied volatility of the options

call_put instance-attribute

call_put

Indicator for call (1) or put (-1) options

quantflow.options.surface.OptionPrice pydantic-model

Bases: BaseModel

Represents the price of an option quoted in the market along with its metadata and implied volatility information.

Fields:

price pydantic-field

price

Price of the option as a percentage of the forward price

meta pydantic-field

meta

Metadata of the option price

forward pydantic-field

forward = ZERO

Forward price of the underlying

ttm pydantic-field

ttm = 0

Time to maturity in years

iv pydantic-field

iv = 0

Implied volatility of the option

side pydantic-field

side = BID

Side of the market for the option price

converged pydantic-field

converged = False

Flag indicating if implied vol calculation converged

strike property

strike

maturity property

maturity

option_type property

option_type

log_strike property

log_strike

Log strike of the option, calculated as log(strike/forward)

price_in_forward_space property

price_in_forward_space

Price of the option as a percentage of the forward price

price_bp property

price_bp

Price of the option in basis points, calculated as price in forward space multiplied by 10000

price_in_quote property

price_in_quote

Price of the option in quote currency

price_intrinsic property

price_intrinsic

Intrinsic price of the option in forward space, which is the price if the option had zero time value

price_time property

price_time

Time value of the option in forward space, which is the price minus its intrinsic value

call_price property

call_price

call price in forward space

use put-call parity to calculate the call price if a put

put_price property

put_price

put price in forward space

use put-call parity to calculate the put price if a call

is_in_the_money

is_in_the_money(forward)
Source code in quantflow/options/surface.py
def is_in_the_money(self, forward: Decimal) -> bool:
    return self.meta.is_in_the_money(forward)

calculate_price

calculate_price()
Source code in quantflow/options/surface.py
def calculate_price(self) -> Self:
    price = Decimal(
        sigfig(
            black_price(
                np.asarray(self.log_strike),
                self.iv,
                self.ttm,
                1 if self.option_type.is_call() else -1,
            ).sum(),
            8,
        )
    )
    self.price = price if self.meta.inverse else price * self.forward
    return self

info_dict

info_dict(open_interest=ZERO, volume=ZERO)
Source code in quantflow/options/surface.py
def info_dict(
    self,
    open_interest: Decimal = ZERO,
    volume: Decimal = ZERO,
) -> dict[str, Any]:
    return dict(
        strike=float(self.strike),
        forward=float(self.forward),
        maturity=self.maturity,
        log_strike=self.log_strike,
        moneyness=moneyness_from_log_strike(self.log_strike, self.ttm),
        ttm=self.ttm,
        iv=self.iv,
        price=float(self.price_in_forward_space),
        price_bp=float(self.price_bp),
        price_quote=float(self.price_in_quote),
        type=str(self.option_type),
        side=str(self.side),
        open_interest=float(open_interest),
        volume=float(volume),
    )

info

info(open_interest=ZERO, volume=ZERO)

Return a structured OptionInfo representation of this option price

Source code in quantflow/options/surface.py
def info(
    self,
    open_interest: Decimal = ZERO,
    volume: Decimal = ZERO,
) -> OptionInfo:
    """Return a structured [OptionInfo][quantflow.options.surface.OptionInfo]
    representation of this option price"""
    return OptionInfo(
        strike=self.strike,
        forward=self.forward,
        maturity=self.maturity,
        log_strike=to_decimal(self.log_strike),
        moneyness=to_decimal(
            float(moneyness_from_log_strike(self.log_strike, self.ttm))
        ),
        ttm=to_decimal(self.ttm),
        iv=to_decimal(self.iv),
        price=self.price_in_forward_space,
        price_bp=self.price_bp,
        price_quote=self.price_in_quote,
        option_type=self.option_type,
        side=self.side,
        open_interest=open_interest,
        volume=volume,
    )

quantflow.options.surface.OptionPrices pydantic-model

Bases: BaseModel, Generic[S]

Represents the market for a single option contract (identified by its strike, maturity and option type), holding the bid and ask sides as separate OptionPrice objects.

Fields:

security pydantic-field

security

The underlying security of the price

meta pydantic-field

meta

Metadata for the option prices

bid pydantic-field

bid

Bid option price

ask pydantic-field

ask

Ask option price

open_interest pydantic-field

open_interest = ZERO

Open interest of the spot price

volume pydantic-field

volume = ZERO

Total volume traded

converged property

converged

Check if the implied volatility calculation has converged for both bid and ask

mid property

mid

Calculate the mid option price by averaging the bid and ask prices

spread property

spread

Calculate the bid-ask spread

price

price()

Convert the option prices to a PriceVolume object

Source code in quantflow/options/surface.py
def price(self) -> PriceVolume:
    """Convert the option prices to a PriceVolume object"""
    return PriceVolume(
        bid=self.bid.price,
        ask=self.ask.price,
        volume=self.volume,
        open_interest=self.open_interest,
    )

iv_bid_ask_spread

iv_bid_ask_spread()

Calculate the bid-ask spread of the implied volatility

Source code in quantflow/options/surface.py
def iv_bid_ask_spread(self) -> float:
    """Calculate the bid-ask spread of the implied volatility"""
    return self.ask.iv - self.bid.iv

iv_mid

iv_mid()

Calculate the mid implied volatility

Source code in quantflow/options/surface.py
def iv_mid(self) -> float:
    """Calculate the mid implied volatility"""
    return (self.bid.iv + self.ask.iv) / 2

is_in_the_money

is_in_the_money(forward)

Check if the option is in the money given the forward price

Source code in quantflow/options/surface.py
def is_in_the_money(self, forward: Decimal) -> bool:
    """Check if the option is in the money given the forward price"""
    return self.meta.is_in_the_money(forward)

disable

disable()

Disable the option by setting its implied volatility convergence to False

Source code in quantflow/options/surface.py
def disable(self) -> None:
    """Disable the option by setting its implied volatility convergence to False"""
    self.bid.converged = False
    self.ask.converged = False

prices

prices(forward, ttm, *, initial_vol=INITIAL_VOL)

Iterator over bid/ask option prices

PARAMETER DESCRIPTION
forward

Forward price of the underlying asset

TYPE: Decimal

ttm

Time to maturity in years

TYPE: float

initial_vol

Initial volatility for the root finding algorithm

TYPE: float DEFAULT: INITIAL_VOL

Source code in quantflow/options/surface.py
def prices(
    self,
    forward: Annotated[Decimal, Doc("Forward price of the underlying asset")],
    ttm: Annotated[float, Doc("Time to maturity in years")],
    *,
    initial_vol: Annotated[
        float, Doc("Initial volatility for the root finding algorithm")
    ] = INITIAL_VOL,
) -> Iterator[OptionPrice]:
    """Iterator over bid/ask option prices"""
    for o in (self.bid, self.ask):
        o.forward = forward
        o.ttm = ttm
        if not o.iv:
            o.iv = initial_vol
        yield o

inputs

inputs()

Convert the option prices to an OptionInput instance

Source code in quantflow/options/surface.py
def inputs(self) -> OptionInput:
    """Convert the option prices to an OptionInput instance"""
    return OptionInput(
        bid=self.bid.price,
        ask=self.ask.price,
        open_interest=self.open_interest,
        volume=self.volume,
        strike=self.meta.strike,
        maturity=self.meta.maturity,
        option_type=self.meta.option_type,
        iv_bid=to_decimal_or_none(
            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.iv) else round(self.ask.iv, 7)
        ),
    )

quantflow.options.surface.OptionSelection

Bases: Enum

Option selection method

This enum is used to select which one between calls and puts are used for calculating implied volatility and other operations

BEST class-attribute instance-attribute

BEST = auto()

Select the OTM option but blend call and put implied volatilities near the money. The blending weight transitions linearly from 50/50 at moneyness 0 to pure OTM at the moneyness threshold.

OTM class-attribute instance-attribute

OTM = auto()

Select Out of the Money options only, where their intrinsic value is zero

CALL class-attribute instance-attribute

CALL = auto()

Select the call options only

PUT class-attribute instance-attribute

PUT = auto()

Select the put options only

ALL class-attribute instance-attribute

ALL = auto()

Select all options regardless of their moneyness

quantflow.options.surface.surface_from_inputs

surface_from_inputs(inputs)

Helper function to build a volatility surface from a VolSurfaceInputs instance

PARAMETER DESCRIPTION
inputs

Volatility surface input data

TYPE: VolSurfaceInputs

Source code in quantflow/options/surface.py
def surface_from_inputs(
    inputs: Annotated[VolSurfaceInputs, Doc("Volatility surface input data")],
) -> VolSurface[DefaultVolSecurity]:
    """Helper function to build a volatility surface from a
    [VolSurfaceInputs][quantflow.options.inputs.VolSurfaceInputs] instance
    """
    loader = VolSurfaceLoader(
        asset=inputs.asset,
        quote_curve=inputs.quote_curve,
        asset_curve=inputs.asset_curve,
    )
    for input in inputs.inputs:
        loader.add(input)
    return loader.surface()

Vol Surface Inputs

quantflow.options.inputs.OptionType

Bases: StrEnum

Type of option

CALL class-attribute instance-attribute

CALL = auto()

PUT class-attribute instance-attribute

PUT = auto()

is_call

is_call()
Source code in quantflow/options/inputs.py
def is_call(self) -> bool:
    return self is OptionType.CALL

is_put

is_put()
Source code in quantflow/options/inputs.py
def is_put(self) -> bool:
    return self is OptionType.PUT

call_put

call_put()

Return 1 for call options and -1 for put options

Source code in quantflow/options/inputs.py
def call_put(self) -> int:
    """Return 1 for call options and -1 for put options"""
    return 1 if self is OptionType.CALL else -1

quantflow.options.inputs.OptionMetadata pydantic-model

Bases: BaseModel

Represents the metadata of an option, including its strike, type, maturity, and other relevant information.

Fields:

strike pydantic-field

strike

Strike price of the option

option_type pydantic-field

option_type

Type of the option, call or put

maturity pydantic-field

maturity

Maturity date of the option

inverse pydantic-field

inverse = True

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)

is_in_the_money

is_in_the_money(forward)

Check if the option is in the money given the forward price

Source code in quantflow/options/inputs.py
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

quantflow.options.inputs.VolSurfaceInputs pydantic-model

Bases: BaseModel

Class representing the inputs for a volatility surface

Fields:

asset pydantic-field

asset

Underlying asset of the volatility surface

asset_curve pydantic-field

asset_curve

Asset yield curve

quote_curve pydantic-field

quote_curve

Quote yield curve

inputs pydantic-field

inputs

List of inputs for the volatility surface

quantflow.options.inputs.VolSurfaceInput module-attribute

VolSurfaceInput = SpotInput | ForwardInput | OptionInput

quantflow.options.inputs.SpotInput pydantic-model

Bases: PriceVolume

Input data for a spot contract in the volatility surface

Fields:

security_type pydantic-field

security_type = SPOT

Type of security for the volatility surface

bid pydantic-field

bid

Bid price

ask pydantic-field

ask

Ask price

mid property

mid

Calculate the mid price by averaging the bid and ask prices

spread property

spread

Calculate the bid-ask spread

bp_spread property

bp_spread

Bid-ask spread in basis points, calculated as spread divided by mid price and multiplied by 10000

open_interest pydantic-field

open_interest = ZERO

Open interest of the security

volume pydantic-field

volume = ZERO

Volume of the security

is_valid

is_valid()

Check if the price is valid, which means the bid is less than or equal to the ask

Source code in quantflow/utils/price.py
def is_valid(self) -> bool:
    """Check if the price is valid, which means the bid is less than
    or equal to the ask"""
    return self.bid <= self.ask

quantflow.options.inputs.ForwardInput pydantic-model

Bases: PriceVolume

Input data for a forward contract in the volatility surface

Fields:

maturity pydantic-field

maturity

Expiry date of the forward contract

security_type pydantic-field

security_type = FORWARD

Type of security for the volatility surface

bid pydantic-field

bid

Bid price

ask pydantic-field

ask

Ask price

mid property

mid

Calculate the mid price by averaging the bid and ask prices

spread property

spread

Calculate the bid-ask spread

bp_spread property

bp_spread

Bid-ask spread in basis points, calculated as spread divided by mid price and multiplied by 10000

open_interest pydantic-field

open_interest = ZERO

Open interest of the security

volume pydantic-field

volume = ZERO

Volume of the security

is_valid

is_valid()

Check if the price is valid, which means the bid is less than or equal to the ask

Source code in quantflow/utils/price.py
def is_valid(self) -> bool:
    """Check if the price is valid, which means the bid is less than
    or equal to the ask"""
    return self.bid <= self.ask

quantflow.options.inputs.OptionInput pydantic-model

Bases: PriceVolume, OptionMetadata

Input data for an option in the volatility surface

Fields:

security_type pydantic-field

security_type = OPTION

Type of security for the volatility surface

iv_bid pydantic-field

iv_bid = None

Implied volatility based on the bid price as decimal number (e.g. 0.2 for 20%)

iv_ask pydantic-field

iv_ask = None

Implied volatility based on the ask price as decimal number (e.g. 0.2 for 20%)

strike pydantic-field

strike

Strike price of the option

option_type pydantic-field

option_type

Type of the option, call or put

maturity pydantic-field

maturity

Maturity date of the option

inverse pydantic-field

inverse = True

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)

bid pydantic-field

bid

Bid price

ask pydantic-field

ask

Ask price

mid property

mid

Calculate the mid price by averaging the bid and ask prices

spread property

spread

Calculate the bid-ask spread

bp_spread property

bp_spread

Bid-ask spread in basis points, calculated as spread divided by mid price and multiplied by 10000

open_interest pydantic-field

open_interest = ZERO

Open interest of the security

volume pydantic-field

volume = ZERO

Volume of the security

is_in_the_money

is_in_the_money(forward)

Check if the option is in the money given the forward price

Source code in quantflow/options/inputs.py
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

is_valid

is_valid()

Check if the price is valid, which means the bid is less than or equal to the ask

Source code in quantflow/utils/price.py
def is_valid(self) -> bool:
    """Check if the price is valid, which means the bid is less than
    or equal to the ask"""
    return self.bid <= self.ask