Source code for yieldcurves.optioncurves

# -*- coding: utf-8 -*-
# dcf
# ---
# A Python library for generating discounted cashflows.
#
# Author:   sonntagsgesicht, based on a fork of Deutsche Postbank [pbrisk]
# Version:  0.7, copyright Sunday, 22 May 2022
# Website:  https://github.com/sonntagsgesicht/dcf
# License:  Apache License 2.0 (see LICENSE file)

from typing import Callable

from prettyclass import prettyclass

from .datecurves import _DAYS_IN_YEAR
from .optionpricing import (OptionPricingFormula, Intrinsic, Bachelier,
                            Black76, DisplacedBlack76)


[docs] @prettyclass(init=False) class OptionPricingCurve: """model to derive expected option payoff cashflows""" if True: DELTA_SHIFT = 0.0001 r"""finite difference to calculate numerical delta sensitivities""" DELTA_SCALE = 0.0001 r"""factor to express numerical delta sensitivities usually in a value of a basis point (bpv) Let $\delta$ be the **DELTA_SHIFT** and $\epsilon$ be the **DELTA_SCALE** and $f$ a forward $F$ sensitive function such that $$f' = \frac{df}{dF} \approx \Delta_f(F) = \frac{f(F+\delta) - f(x)}{\delta/\epsilon}.$$ """ VEGA_SHIFT = 0.01 r"""finite difference to calculate numerical vega sensitivities """ VEGA_SCALE = 0.01 r"""factor to express numerical vega sensitivities Let $\delta$ be the **VEGA_SHIFT** and $\epsilon$ be the **VEGA_SCALE** and $f$ a volatility $\nu$ sensitive function such that $$f'_\nu = \frac{df}{d\nu} \approx \mathcal{V}_f(\nu) = \frac{f(\nu+\delta) - f(\nu)}{\delta/\epsilon}.$$ """ THETA_SHIFT = 1 / _DAYS_IN_YEAR r"""finite difference to calculate numerical theta sensitivities usually one day (1/365.25)""" THETA_SCALE = 1 / _DAYS_IN_YEAR r"""factor to express numerical theta sensitivities usually one day (1/365.25) Let $\delta$ be the **THETA_SHIFT** and $\epsilon$ be the **THETA_SCALE** and $f$ a tau $\tau(t,T)$ sensitive function with valuation date $t$ and option maturity date $T$ such that $$\dot{f} = \frac{df}{dt} \approx \Theta_f(t) = \frac{f(\tau(t,T)+\delta) - f(\tau(t,T))}{\delta/\epsilon}.$$ """
[docs] @classmethod def intrinsic(cls, curve, volatility=None): volatility = volatility return cls(curve, formula=Intrinsic())
[docs] @classmethod def bachelier(cls, curve, volatility=None): return cls(curve, formula=Bachelier(), volatility=volatility)
[docs] @classmethod def black76(cls, curve, volatility=None): return cls(curve, formula=Black76(), volatility=volatility)
[docs] @classmethod def displaced_black76(cls, curve, volatility=None, *, displacement=0.0): formula = DisplacedBlack76(displacement=displacement) return cls(curve, formula=formula, volatility=volatility)
def __init__(self, curve: Callable, *, formula: OptionPricingFormula = None, volatility: Callable = None, bump_greeks: bool = False, bump_binary: float | bool | None = 0.0001): """curve extension for option pricing :param curve: forward curve to derives forward values :param formula: call option pricing formula callable which must provide float consuming signature **formula(tau, strike, forward, volatility)** here **tau** is the time to expiry **y-x** :param volatility: volatility curve :param bump_greeks: **bool** - if **True** Greeks, i.e. sensitivities/derivatives, are derived numerically. If **False** analytics functions are used, if given. See also |OptionPricingFormula()|. (optional; default is **False**) :param bump_binary: finite difference to calculate Binary option payoffs numerically i.e. digital options are derived numerically via call/ put spreads. If **False**, **None** or **0.0** analytics functions are used, e.g. **formula.binary_call** if present. See also |OptionPricingFormula()|. (optional; default is 0.0001) """ self.curve = curve self.formula = formula self.volatility = volatility self.bump_binary = bump_binary self.bump_greeks = bump_greeks def __call__(self, x, y=None): return self.forward(x, y)
[docs] def forward(self, x, y=None): if callable(self.curve): if y is None: return self.curve(x) return self.curve(x, y) return self.curve
[docs] def details(self, x, y=None, *, strike=None, **__): """model parameter details :param x: option valuation date :param y: option expiry date (also fixing date) (optional; if not given **x** will be expiry date) :param strike: option strike value (optional; default **None**, i.e. *at-the-money*) :return: dict() """ if y is None: x, y = 0.0, x tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) option_model = getattr(self.formula, '__qualname__', str(self.formula)) details = { 'valuation date': x, 'expiry date': y, 'time to expiry': tau, 'strike': strike, 'forward': fwd, 'volatility': vol, 'option model': option_model.replace('()', ''), 'forward-curve-id': f"id{id(self.curve)}", 'volatility-curve-id': f"id{id(self.volatility)}", 'option-curve-id': f"id{id(self)}" } if self.volatility is None: details.pop('volatility', None) details.pop('volatility-curve-id', None) return {k: v for k, v in details.items() if v is not None}
def _tsfv(self, x, y=None, *, strike=None): """tau, strike, forward, volatility :param x: valuation date :param y: expiry date :param strike: strike :return: tuple """ if y is None: x, y = 0.0, x fwd = self.forward(y) if strike is None: strike = fwd vol = self.volatility if callable(vol): # assume terminal vol curve vol = vol(y) if callable(vol): # for vol smile models vol = vol(strike, fwd) tau = y - x return tau, strike, fwd, vol # --- vanilla (uses only '_tsfv', 'formula' and 'bump_...')
[docs] def call(self, x, y=None, *, strike=None): r""" value of a call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F(T)$ :returns: $C_K(F(T))=E[\max(F(T)-K, 0)]$ """ tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if self.formula is None or not vol or not tau: return max(0.0, fwd - strike) return self.formula(tau, strike, fwd, vol)
[docs] def put(self, x, y=None, *, strike=None): r""" value of a put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $P_K(F(T))=E[\max(K-F(T), 0)]$ Note $P_K(F(T))$ is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_: $$P_K(F(T)) = K - F(T) + C_K(F(T))$$ """ fwd = self(x, y) call = self.call(x, y, strike=strike) if strike is None: strike = fwd return strike - fwd + call # put/call parity
[docs] def call_delta(self, x, y=None, *, strike=None): r""" delta sensitivity of a call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Delta_{C_K(F)} = \frac{d}{d F} C_K(F)$ $\Delta_{C_K(F)}$ is the first derivative of $C_K(F)$ in unterlying direction $F$. """ scale = self.__class__.DELTA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if fwd < strike else 1.0 * scale # cadlag if not self.bump_greeks \ and hasattr(self.formula, 'delta'): delta = self.formula.delta(tau, strike, fwd, vol) if delta is not None: return delta * scale shift = self.__class__.DELTA_SHIFT delta = self.formula(tau, strike, fwd + shift, vol) delta -= self.formula(tau, strike, fwd, vol) delta = delta / shift return delta * scale
[docs] def put_delta(self, x, y=None, *, strike=None): r""" delta sensitivity of a put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Delta_{P_K(F)} = \frac{d}{d F} P_K(F)$ $\Delta_{P_K(F)}$ is the first derivative of $P_K(F)$ in underlying direction $F$ and is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_, too: $$\Gamma_{P_K(F)} = \Delta_{C_K(F)} - 1$$ Note, here $1$ is actualy scaled by |OptionPricingCurve.DELTA_SCALE|. """ scale = self.__class__.DELTA_SCALE # put/call parity return self.call_delta(x, y, strike=strike) - 1.0 * scale
[docs] def call_gamma(self, x, y=None, *, strike=None): r""" gamma sensitivity of a call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Gamma_{C_K(F)} = \frac{d^2}{d F^2} C_K(F)$ $\Gamma_{C_K(F)}$ is the second derivative of $C_K(F)$ in unterlying direction $F$. """ scale = self.__class__.DELTA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if not self.bump_greeks \ and hasattr(self.formula, 'gamma'): gamma = self.formula.gamma(tau, strike, fwd, vol) if gamma is not None: return gamma * (scale ** 2) shift = self.__class__.DELTA_SHIFT gamma = self.formula(tau, strike, fwd + shift, vol) gamma -= 2 * self.formula(tau, strike, fwd, vol) gamma += self.formula(tau, strike, fwd - shift, vol) gamma *= (scale / shift) ** 2 return gamma
[docs] def put_gamma(self, x, y=None, *, strike=None): r""" gamma sensitivity of a put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Gamma_{P_K(F)} = \frac{d^2}{d F^2} P_K(F)$ $\Gamma_{P_K(F)}$ is the second derivative of $P_K(F)$ in unterlying direction $F$ and is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_, too: $$\Gamma_{P_K(F)} = \Gamma_{C_K(F)}$$ """ return self.call_gamma(x, y, strike=strike) # put/call parity
[docs] def call_vega(self, x, y=None, *, strike=None): r""" vega sensitivity of a call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\mathcal{V}_{C_K(F)} = \frac{d}{d v} C_K(F)$ $\mathcal{V}_{C_K(F)}$ is the first derivative of $C_K(F)$ in volatility parameter direction $v$. """ scale = self.__class__.VEGA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if (not self.bump_greeks and hasattr(self.formula, 'vega')): vega = self.formula.vega(tau, strike, fwd, vol) if vega is not None: return vega * scale shift = self.__class__.VEGA_SHIFT vega = self.formula(tau, strike, fwd, vol + shift) vega -= self.formula(tau, strike, fwd, vol) vega *= scale / shift return vega
[docs] def put_vega(self, x, y=None, *, strike=None): r""" vega sensitivity of a put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\mathcal{V}_{P_K(F)} = \frac{d}{d v} P_K(F)$ $\mathcal{V}_{P_K(F)}$ is the first derivative of $P_K(F)$ in volatility parameter direction $v$ and is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_, too: $$\mathcal{V}_{P_K(F)} = \mathcal{V}_{C_K(F)}$$ """ return self.call_vega(x, y, strike=strike)
[docs] def call_theta(self, x, y=None, *, strike=None): r""" tau sensitivity of a call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Theta_{C_K(F)} = \frac{d}{d t} C_K(F)$ $\Theta_{C_K(F)}$ is the first derivative of $C_K(F)$ in tau parameter direction, i.e. valuation date $t$. """ scale = self.__class__.THETA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if not self.bump_greeks \ and hasattr(self.formula, 'theta'): theta = self.formula.theta(tau, strike, fwd, vol) if theta is not None: return theta * scale shift = self.__class__.THETA_SHIFT theta = self.formula(tau + shift, strike, fwd, vol) theta -= self.formula(tau, strike, fwd, vol) return theta * scale
[docs] def put_theta(self, x, y=None, *, strike=None): r""" tau sensitivity of a put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Theta_{P_K(F)} = \frac{d}{d t} P_K(F)$ $\Theta_{P_K(F)}$ is the first derivative of $P_K(F)$ in tau parameter direction, i.e. valuation date $t$. """ return self.call_theta(x, y, strike=strike)
# --- binary (requires only '_tsfv' and 'option_...' and 'bump_...')
[docs] def binary_call(self, x, y=None, *, strike=None): r""" value of a binary call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\delta C_K(F(T))=E[ ]$ """ if self.bump_binary is None: tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if self.formula is None or vol is None or not tau: return 1.0 if strike < fwd else 0.0 call = self.formula.binary(tau, strike, fwd, vol) if call is not None: return call shift = self.bump_binary or 0.0001 strike = self(x, y) if strike is None else strike call_spread = self.call(x, y, strike=strike + shift / 2) call_spread -= self.call(x, y, strike=strike - shift / 2) return call_spread / shift
[docs] def binary_put(self, x, y=None, *, strike=None): r""" value of a put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\delta P_K(F(T))=E[ ]$ Note $P_K(F(T))$ is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_: $$\delta P_K(F(T)) = 1.0 - \delta C_K(F(T))$$ """ call = self.binary_call(x, y, strike=strike) return 1.0 - call # put/call parity
[docs] def binary_call_delta(self, x, y=None, *, strike=None): r""" delta sensitivity of a binary call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Delta_{ \delta C_K(F)} = \frac{d}{d F} \delta C_K(F)$ $\Delta_{ \delta C_K(F)}$ is the first derivative of $\delta C_K(F)$ in underlying direction $F$. """ scale = self.__class__.DELTA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if fwd < strike else 1.0 * scale # cadlag if not self.bump_greeks and \ hasattr(self.formula, 'binary_delta'): delta = self.formula.binary_delta( tau, strike, fwd, vol) if delta is not None: return delta * scale shift = self.__class__.DELTA_SHIFT delta = \ self.formula.binary(tau, strike, fwd + shift, vol) delta -= self.formula.binary(tau, strike, fwd, vol) delta = delta / shift return delta * scale
[docs] def binary_put_delta(self, x, y=None, *, strike=None): r""" delta sensitivity of a binary put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Delta_{ \delta P_K(F)} = \frac{d}{d F} \delta P_K(F)$ $\Delta_{ \delta P_K(F)}$ is the first derivative of $P_K(F)$ in unterlying direction $F$ and is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_, too: $$\Gamma_{P_K(F)} = \Delta_{ \delta C_K(F)} - 1$$ Note, here $1$ is actually scaled by |OptionPricingCurve.DELTA_SCALE|. """ scale = self.__class__.DELTA_SCALE # put/call parity return self.binary_call_delta(x, y, strike=strike) - 1.0 * scale
[docs] def binary_call_gamma(self, x, y=None, *, strike=None): r""" gamma sensitivity of a binary call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Gamma_{ \delta C_K(F)} = \frac{d^2}{d F^2} \delta C_K(F)$ $\Gamma_{ \delta C_K(F)}$ is the second derivative of $ \delta C_K(F)$ in underlying direction $F$. """ scale = self.__class__.DELTA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if not self.bump_greeks and \ hasattr(self.formula, 'binary_gamma'): gamma = self.formula.gamma(tau, strike, fwd, vol) if gamma is not None: return gamma * (scale ** 2) shift = self.__class__.DELTA_SHIFT gamma = \ self.formula.binary(tau, strike, fwd + shift, vol) gamma -= 2 * self.formula.binary(tau, strike, fwd, vol) gamma += \ self.formula.binary(tau, strike, fwd - shift, vol) gamma *= (scale / shift) ** 2 return gamma
[docs] def binary_put_gamma(self, x, y=None, *, strike=None): r""" gamma sensitivity of a binary put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Gamma_{ \delta P_K(F)} = \frac{d^2}{d F^2} \delta P_K(F)$ $\Gamma_{ \delta P_K(F)}$ is the second derivative of $ \delta P_K(F)$ in unterlying direction $F$ and is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_, too: $$\Gamma_{ \delta P_K(F)} = \Gamma_{ \delta C_K(F)}$$ """ return self.binary_call_gamma(x, y, strike=strike) # put/call parity
[docs] def binary_call_vega(self, x, y=None, *, strike=None): r""" vega sensitivity of a binary call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\mathcal{V}_{ \delta C_K(F)} = \frac{d}{d v} \delta C_K(F)$ $\mathcal{V}_{ \delta C_K(F)}$ is the first derivative of $ \delta C_K(F)$ in volatility parameter direction $v$. """ scale = self.__class__.VEGA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if not self.bump_greeks and \ hasattr(self.formula, 'binary_vega'): vega = self.formula.vega(tau, strike, fwd, vol) if vega is not None: return vega * scale shift = self.__class__.VEGA_SHIFT vega = \ self.formula.binary(tau, strike, fwd, vol + shift) vega -= self.formula.binary(tau, strike, fwd, vol) vega *= scale / shift return vega
[docs] def binary_put_vega(self, x, y=None, *, strike=None): r""" vega sensitivity of a binary put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\mathcal{V}_{ \delta P_K(F)} = \frac{d}{d v} \delta P_K(F)$ $\mathcal{V}_{ \delta P_K(F)}$ is the first derivative of $\delta P_K(F)$ in volatility parameter direction $v$ and is derived by `put-call parity <https://en.wikipedia.org/wiki/Put–call_parity>`_, too: $$\mathcal{V}_{ \delta P_K(F)} = \mathcal{V}_{ \delta C_K(F)}$$ """ return self.binary_call_vega(x, y, strike=strike)
[docs] def binary_call_theta(self, x, y=None, *, strike=None): r""" tau sensitivity of a binary call option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Theta_{ \delta C_K(F)} = \frac{d}{d t} \delta C_K(F)$ $\Theta_{ \delta C_K(F)}$ is the first derivative of $\delta C_K(F)$ in tau parameter direction, i.e. valuation date $t$. """ scale = self.__class__.THETA_SCALE tau, strike, fwd, vol = self._tsfv(x, y, strike=strike) if not vol or not tau: return 0.0 if not self.bump_greeks and \ hasattr(self.formula, 'binary_theta'): theta = self.formula.theta(tau, strike, fwd, vol) if theta is not None: return theta * scale shift = self.__class__.THETA_SHIFT theta = \ self.formula.binary(tau + shift, strike, fwd, vol) theta -= self.formula.binary(tau, strike, fwd, vol) return theta * scale
[docs] def binary_put_theta(self, x, y=None, *, strike=None): r""" tau sensitivity of a binary put option :param x: valuation date $t$ :param y: expiry date $T$ :param strike: option strike price $K$ of underlying $F$ :return: $\Theta_{ \delta P_K(F)} = \frac{d}{d t} \delta P_K(F)$ $\Theta_{ \delta P_K(F)}$ is the first derivative of $\delta P_K(F)$ in tau parameter direction, i.e. valuation date $t$. """ return self.binary_call_theta(x, y, strike=strike)