mafipy/calibrator/sabr.py
#!/bin/python
# -*- coding: utf-8 -*-
from __future__ import division, print_function, absolute_import
import math
import numpy as np
import scipy.optimize
import mafipy.function
# ----------------------------------------------------------------------------
# SABR calibration
# ----------------------------------------------------------------------------
def _guess_alpha_from_vol_atm(underlying, vol_atm, beta):
"""_guess_alpha_from_vol_atm_
guess alpha from volatility at the money
by using Hagan's approximated atm implied volatility.
.. math::
\ln\sigma(F, F)
\\approx \ln \\alpha - (1 - \\beta) \ln F
:param float underlying: must be positive.
:param float vol_atm: volatility at the money. This must be positive.
:param float beta:
:return: alpha.
:rtype: float.
"""
assert(underlying > 0.0)
assert(vol_atm > 0.0)
ln_underlying = math.log(underlying)
ln_vol_atm = math.log(vol_atm)
one_minus_beta = 1.0 - beta
ln_alpha = ln_vol_atm + one_minus_beta * ln_underlying
return math.exp(ln_alpha)
def sabr_caibration_simple(market_vols,
market_strikes,
option_maturity,
beta,
init_alpha=None,
init_rho=0.1,
init_nu=0.1,
nu_lower_bound=1e-8,
tol=1e-10):
"""sabr_caibration_simple
calibrates SABR parametes, alpha, rho and nu to market volatilities
by simultaneously minimizing error of market volatilities.
:param array market_vols: market volatilities.
Middle of elements in the array must be atm volatility.
:param array market_strikes: market strikes.
Middle of elements in the array must be atm strike.
:param float option_maturity:
:param float beta: pre-determined beta.
beta must be within [0, 1].
:param float init_alpha: initial guess of alpha.
Default value is meaningless value, 0.1.
alpha must be positive.
:param float init_rho: initial guess of rho.
Default value is meaningless value, 0.1.
rho must be within [-1, 1].
:param float init_nu: initial guess of nu.
Default value is meaningless value, 0.1.
nu must be positive.
:param float nu_lower_bound:
:param float tol: tolerance of minimization.
:return: alpha, beta, rho, nu.
:rtype: four float value.
:raise AssertionError: if length of `market_vols` is not odd.
:raise AssertionError:
if length of `market_vols` and `market_strikes` are not same.
"""
assert len(market_vols) % 2 == 1, \
"lenght of makret_vols must be odd"
assert len(market_strikes) == len(market_vols), \
"market_vols and market_strikes must be same lenght"
# atm strike is underlying
underlying = market_strikes[int(len(market_strikes) / 2)]
vol_atm = market_vols[int(len(market_vols) / 2)]
if init_alpha is None:
init_alpha = _guess_alpha_from_vol_atm(underlying, vol_atm, beta)
# parameter check
assert(underlying > 0.0)
assert(vol_atm > 0.0)
assert(0.0 <= beta <= 1.0)
assert(init_alpha > 0.0)
assert(-1.0 <= init_rho <= 1.0)
assert(init_nu > 0.0)
# 3-dim func
# (alpha, rho, nu)
def objective_func(alpha_rho_nu):
sabr_vols = [mafipy.function.sabr_implied_vol_hagan(
underlying,
strike,
option_maturity,
alpha_rho_nu[0],
beta,
alpha_rho_nu[1],
alpha_rho_nu[2]) for strike in market_strikes]
return np.linalg.norm(np.subtract(market_vols, sabr_vols)) ** 2
result = scipy.optimize.minimize(
objective_func,
[init_alpha, init_rho, init_nu],
method="L-BFGS-B",
bounds=((0.0, None), (-1.0, 1.0), (nu_lower_bound, None)),
tol=tol)
alpha, rho, nu = result.x
return alpha, beta, rho, nu
def _is_real_and_positive(val):
"""_is_real_and_positive
:param complex val:
:return: True if `val` is real and positive.
:rtype: bool.
"""
if np.isreal(val) and val > 0.0:
return True
return False
def _find_alpha(underlying, option_maturity, vol_atm, beta, rho, nu):
"""_find_alpha
find value of alpha solving the following polynominal equation
with respect to alpha.
.. math::
\\frac{
(1 - \\beta)^{2} \\tau
}{
24F^{2-2\\beta}
}
\\alpha^{3}
+
\\frac{
\\rho\\beta\\nu\\tau
}{
4F^{1-\\beta}
}
\\alpha^{2}
+
\left(
1
+
\\frac{
2 - 3\\rho^{2}
}{
24
}
\\nu^{2} \\tau
\\right)
\\alpha
- \sigma_{\mathrm{atm}} F^{1-\\beta}
= 0
:param float underlying:
:param float option_maturity:
:param float vol_atm:
:param float beta:
:param float rho:
:param float nu:
:return: mininum real positive root of the equiation.
:rtype: float.
:raise ValueError:
if there is no positive real roots.
"""
one_minus_beta = 1.0 - beta
one_minus_beta2 = one_minus_beta ** 2
numerator1 = one_minus_beta2 * option_maturity
denominator1 = 24.0 * (underlying ** (2.0 * one_minus_beta))
coeff1 = numerator1 / denominator1
numerator2 = rho * beta * nu * option_maturity
denominator2 = 4.0 * (underlying ** one_minus_beta)
coeff2 = numerator2 / denominator2
numerator3 = (2.0 - 3.0 * rho * rho) * nu * nu * option_maturity
coeff3 = (1.0 + numerator3 / 24.0)
constant_term = -vol_atm * (underlying ** one_minus_beta)
coeffs = [coeff1, coeff2, coeff3, constant_term]
roots = np.roots(coeffs)
# find smallest real roots
positive_real_roots = [
root.real for root in roots if _is_real_and_positive(root)]
if len(positive_real_roots) == 0:
raise ValueError("""
Find no real positive roots for alpha:
underlying: {0},
option_maturity: {1},
vol_atm: {2},
alpha: {3},
beta: {4},
rho: {5},
nu: {6}""".format(underlying, option_maturity, vol_atm,
roots, beta, rho, nu))
return min(positive_real_roots)
def sabr_caibration_west(market_vols,
market_strikes,
option_maturity,
beta,
init_rho=0.1,
init_nu=0.1):
"""sabr_caibration_west
calibrates SABR parameters to market volatilities by algorithm
in West, G. (2005).
Calibration of the SABR Model in Illiquid Markets.
Applied Mathematical Finance, 12(4), 371–385.
https://doi.org/10.1080/13504860500148672
:param array market_vols: market volatilities.
Middle of elements in the array must be atm volatility.
:param array market_strikes: market strikes corresponded to `market_vol`.
Middle of elements in the array must be atm strike.
:param float option_maturity:
:param float beta: pre-determined beta.
:param float init_rho: initial guess of rho.
Default value is meaningless value, 0.1.
:param float init_nu: initial guess of nu.
Default value is meaningless value, 0.1.
:return: alpha, beta, rho, nu.
:rtype: four float value.
:raise AssertionError: if length of `market_vols` is not odd.
:raise AssertionError:
if length of `market_vols` and `market_strikes` are not same.
"""
assert len(market_vols) % 2 == 1, \
"lenght of makret_vols must be odd"
assert len(market_strikes) == len(market_vols), \
"market_vols and market_strikes must be same lenght"
# atm strike is underlying
underlying = market_strikes[int(len(market_strikes) / 2)]
vol_atm = market_vols[int(len(market_vols) / 2)]
def find_alpha(rho, nu):
return _find_alpha(underlying, option_maturity, vol_atm, beta, rho, nu)
# 2-dim func
# (rho, nu)
def objective_func(rho_nu):
alpha = find_alpha(rho_nu[0], rho_nu[1])
sabr_vols = [mafipy.function.sabr_implied_vol_hagan(
underlying,
strike,
option_maturity,
alpha,
beta,
rho_nu[0],
rho_nu[1]) for strike in market_strikes]
return np.linalg.norm(np.subtract(market_vols, sabr_vols)) ** 2
result = scipy.optimize.minimize(
objective_func,
[init_rho, init_nu],
method="L-BFGS-B",
bounds=((-1.0, 1.0), (0.0, None)))
rho, nu = result.x
alpha = find_alpha(rho, nu)
return alpha, beta, rho, nu