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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"args": [
"-x",
"-vvv",
"quantflow_tests/test_data.py::test_fed_yc",
"quantflow_tests/test_options.py",
]
},
]
Expand Down
4 changes: 4 additions & 0 deletions docs/api/options/black.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ where \(K\) is the strike price and \(F\) is the forward price of the underlying
::: quantflow.options.bs.black_vega

::: quantflow.options.bs.implied_black_volatility

::: quantflow.options.bs.ImpliedVols

::: quantflow.options.bs.ImpliedVol
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# <a href="https://quantmind.github.io/quantflow"><img src="https://raw.githubusercontent.com/quantmind/quantflow/main/notebooks/assets/quantflow-light.svg" width=300 /></a>
# <a href="https://quantmind.github.io/quantflow"><img src="https://raw.githubusercontent.com/quantmind/quantflow/main/docs/assets/quantflow-light.svg" width=300 /></a>

[![PyPI version](https://badge.fury.io/py/quantflow.svg)](https://badge.fury.io/py/quantflow)
[![Python versions](https://img.shields.io/pypi/pyversions/quantflow.svg)](https://pypi.org/project/quantflow)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "quantflow"
version = "0.5.1"
version = "0.6.0"
description = "quantitative analysis"
authors = [ { name = "Luca Sbardella", email = "[email protected]" } ]
license = "BSD-3-Clause"
Expand Down
2 changes: 1 addition & 1 deletion quantflow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Quantitative analysis and pricing"""

__version__ = "0.5.1"
__version__ = "0.6.0"
127 changes: 104 additions & 23 deletions quantflow/options/bs.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
from typing import NamedTuple

import numpy as np
from scipy.optimize import RootResults, newton
from scipy.optimize import newton
from scipy.stats import norm
from typing_extensions import Annotated, Doc

from ..utils.types import FloatArray, FloatArrayLike
from ..utils.types import BoolArray, Float, FloatArray, FloatArrayLike


class ImpliedVol(NamedTuple):
"""Result of implied volatility calculation"""

value: Float
"""The implied volatility in decimals (0.2 for 20%)"""
converged: bool
"""Whether the root finding algorithm converged"""


class ImpliedVols(NamedTuple):
"""Result of root finding algorithm"""

values: FloatArray
"""The implied volatilities in decimals (0.2 for 20%)"""
converged: BoolArray
"""Whether the root finding algorithm converged"""

def single(self) -> ImpliedVol:
"""Return the first implied volatility and convergence status a
a single ImpliedVol"""
if len(self.values) != 1 or len(self.converged) != 1:
raise ValueError("Expected exactly one root and convergence status")
return ImpliedVol(value=self.values[0], converged=self.converged[0])


def black_call(
k: Annotated[np.ndarray, Doc("Vector or single value of log-strikes")],
k: Annotated[FloatArrayLike, Doc("Vector or single value of log-strikes")],
sigma: Annotated[
FloatArrayLike,
Doc(
"Corresponding vector or single value of implied volatilities (0.2 for 20%)"
"Corresponding vector or single value of implied volatilities "
"(0.2 for 20%)"
),
],
ttm: Annotated[
FloatArrayLike, Doc("Corresponding vector or single value of Time to Maturity")
],
) -> np.ndarray:
) -> FloatArrayLike:
kk = np.asarray(k)
return black_price(kk, np.asarray(sigma), np.asarray(ttm), np.ones(kk.shape))


def black_price(
k: Annotated[np.ndarray, Doc("Vector of log-strikes")],
k: Annotated[
FloatArrayLike,
Doc("Vector or single value of log-strikes"),
],
sigma: Annotated[
FloatArrayLike,
Doc(
Expand All @@ -36,7 +67,13 @@ def black_price(
ttm: Annotated[
FloatArrayLike, Doc("Corresponding vector or single value of Time to Maturity")
],
s: Annotated[FloatArrayLike, Doc("Call/Put Flag (1 for call, -1 for put)")],
s: Annotated[
FloatArrayLike,
Doc(
"Corresponding vector or single value of call/put flag "
"(1 for call, -1 for put)"
),
],
) -> np.ndarray:
r"""Calculate the Black call/put option prices in forward terms
from the following params
Expand All @@ -61,17 +98,23 @@ def black_price(


def black_delta(
k: Annotated[np.ndarray, Doc("a vector of moneyness, see above")],
k: Annotated[np.ndarray, Doc("Vector of log-strikes")],
sigma: Annotated[
FloatArrayLike,
Doc("a corresponding vector of implied volatilities (0.2 for 20%)"),
Doc(
"Corresponding vector or single value of implied volatilities "
"(0.2 for 20%)"
),
],
ttm: Annotated[
FloatArrayLike, Doc("Corresponding vector or single value of Time to Maturity")
],
s: Annotated[
FloatArrayLike,
Doc("Call/Put vector or single value Flag (1 for call, -1 for put)"),
Doc(
"Corresponding vector or single value of call/put flag "
"(1 for call, -1 for put)"
),
],
) -> np.ndarray:
r"""Calculate the Black call/put option delta from the moneyness,
Expand All @@ -91,13 +134,18 @@ def black_delta(


def black_vega(
k: Annotated[np.ndarray, Doc("a vector of moneyness, see above")],
k: Annotated[FloatArrayLike, Doc("Vector of log-strikes")],
sigma: Annotated[
np.ndarray, Doc("corresponding vector of implied volatilities (0.2 for 20%)")
FloatArrayLike,
Doc(
"Corresponding vector or single value of implied volatilities (0.2 for 20%)"
),
],
ttm: Annotated[FloatArrayLike, Doc("Time to Maturity")],
) -> np.ndarray:
r"""Calculate the Black option vega from the moneyness,
ttm: Annotated[
FloatArrayLike, Doc("Corresponding vector or single value of Time to Maturity")
],
) -> FloatArrayLike:
r"""Calculate the Black option vega from the log-strikes,
volatility and time to maturity. The vega is the same for calls and puts.

$$
Expand All @@ -117,16 +165,49 @@ def black_vega(


def implied_black_volatility(
k: Annotated[np.ndarray, Doc("Vector of log strikes")],
price: Annotated[np.ndarray, Doc("Corresponding vector of option_price/forward")],
ttm: Annotated[FloatArrayLike, Doc("Time to Maturity")],
initial_sigma: Annotated[FloatArray, Doc("Initial Volatility")],
call_put: Annotated[FloatArrayLike, Doc("Call/Put Flag")],
) -> RootResults:
"""Calculate the implied block volatility via Newton's method"""
return newton(
k: Annotated[
FloatArrayLike,
Doc("Vector or scalar of log strikes"),
],
price: Annotated[
FloatArrayLike,
Doc(
"Corresponding vector or scalar of option price in forward terms "
"(price divided by forward price)"
),
],
ttm: Annotated[
FloatArrayLike,
Doc("Corresponding vector or single value of Time to Maturity"),
],
initial_sigma: Annotated[
FloatArrayLike,
Doc("Corresponding vector or single value of initial volatility"),
],
call_put: Annotated[
FloatArrayLike,
Doc(
"Corresponding vector or single value of call/put flag "
"(1 for call, -1 for put)"
),
],
) -> ImpliedVols:
"""Calculate the implied black volatility via Newton's method

It returns a scipy `RootResults` object which contains the implied volatility
in the `root` attribute. Implied volatility is in decimals (0.2 for 20%).
"""
if not np.isscalar(k) and np.isscalar(initial_sigma):
initial_sigma = np.full_like(k, initial_sigma)
result = newton(
lambda x: black_price(k, x, ttm, call_put) - price,
initial_sigma,
fprime=lambda x: black_vega(k, x, ttm),
full_output=True,
)
if hasattr(result, "root"):
return ImpliedVols(values=result.root, converged=result.converged)
else:
return ImpliedVols(
values=np.asarray([result[0]]), converged=np.asarray([result[1]])
)
8 changes: 4 additions & 4 deletions quantflow/options/calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class OptionEntry:
ttm: float
moneyness: float
options: list[OptionPrice] = field(default_factory=list)
_prince_range: Bounds | None = None
_price_range: Bounds | None = None

def implied_vol_range(self) -> Bounds:
"""Get the range of implied volatilities"""
Expand All @@ -50,10 +50,10 @@ def residual(self, price: float) -> float:

def price_range(self) -> Bounds:
"""Get the range of prices"""
if self._prince_range is None:
if self._price_range is None:
prices = tuple(float(option.call_price) for option in self.options)
self._prince_range = Bounds(min(prices), max(prices))
return self._prince_range
self._price_range = Bounds(min(prices), max(prices))
return self._price_range


class VolModelCalibration(BaseModel, ABC, Generic[M], arbitrary_types_allowed=True):
Expand Down
29 changes: 18 additions & 11 deletions quantflow/options/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

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

from pydantic import BaseModel, Field

from quantflow.utils.numbers import ZERO
from quantflow.utils.numbers import ZERO, DecimalNumber

P = TypeVar("P")

Expand Down Expand Up @@ -70,12 +69,12 @@ def option(cls) -> Self:
class VolSurfaceInput(BaseModel):
"""Base class for volatility surface inputs"""

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


class SpotInput(VolSurfaceInput):
Expand All @@ -100,18 +99,26 @@ class ForwardInput(VolSurfaceInput):
class OptionInput(VolSurfaceInput):
"""Input data for an option in the volatility surface"""

strike: Decimal = Field(description="Strike price of the option")
strike: DecimalNumber = Field(description="Strike price of the option")
maturity: datetime = Field(description="Expiry date of the option")
option_type: OptionType = Field(description="Type of the option - call or put")
security_type: VolSecurityType = Field(
default=VolSecurityType.option,
description="Type of security for the volatility surface",
)
iv_bid: Decimal | None = Field(
default=None, description="Implied volatility based on the bid price"
iv_bid: DecimalNumber | None = Field(
default=None,
description=(
"Implied volatility based on the bid price as decimal number "
"(e.g. 0.2 for 20%)"
),
)
iv_ask: Decimal | None = Field(
default=None, description="Implied volatility based on the ask price"
iv_ask: DecimalNumber | None = Field(
default=None,
description=(
"Implied volatility based on the ask price as decimal number "
"(e.g. 0.2 for 20%)"
),
)


Expand Down
8 changes: 5 additions & 3 deletions quantflow/options/pricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def implied_vols(self) -> FloatArray:
ttm=self.ttm,
initial_sigma=0.5 * np.ones_like(self.moneyness),
call_put=1.0,
).root
).values

@property
def df(self) -> pd.DataFrame:
Expand Down Expand Up @@ -97,7 +97,9 @@ def max_moneyness_ttm(
def black(self) -> MaturityPricer:
"""Calculate the Maturity Result for the Black model with same std"""
return self._replace(
call=black_call(self.moneyness, self.std / np.sqrt(self.ttm), ttm=self.ttm),
call=np.asarray(
black_call(self.moneyness, self.std / np.sqrt(self.ttm), ttm=self.ttm)
),
name="Black",
)

Expand All @@ -116,7 +118,7 @@ class OptionPricer(BaseModel, Generic[M], arbitrary_types_allowed=True):
)
"""Cache for :class:`.MaturityPricer` for different time to maturity"""
n: int = 128
"""NUmber of discretization points for the marginal distribution"""
"""Number of discretization points for the marginal distribution"""
max_moneyness_ttm: float = 1.5
"""Max time-adjusted moneyness to calculate prices"""

Expand Down
Loading
Loading