cdxcore.bs#

Basic Black & Scholes pricing routines.

Overview#

This module offers with the bs instance of cdxcore.bs.BS a basic Black & Scholes pricing framework for the drift-less case:

from cdxcore.bs import bs
call = bs.price(1.,vol=0.2,sqrtT=0.1)

Aside from the respective pricing functions and greeks, bs object also offers with cdxcore.bs.BS.implied() a “mass” implied volatility solver. It can solve implied volatilities for a large number of options in one big Euler/bisection search.

The module contains the cdxcore.bs.BS, whose use case is to define the member cdxcore.bs.bs which provides the pricing functionality.

Import#

from cdxcore.bs import bs

Documentation#

Module Attributes

bs

Main instance of the cdxcore.bs.BS class.

Classes

BS()

Base class for the computation of Black & Scholes analytics in drift-less ("pure") price domain.

BSFLAGS(*values)

class cdxcore.bs.BS[source]#

Bases: object

Base class for the computation of Black & Scholes analytics in drift-less (“pure”) price domain.

The class is synthactic sugar; simply use the one instance bs via:

from cdxcore.bs import bs

Then you can do:

call = bs.price( 1., 0.2, sqrtT=0.5 )
vega = bs.vega( 1., 0.2, sqrtT=0.5 )

The most useful function is bs.implied documented under cdxcore.bs.BS.implied`().

DELTA = 2#

Flag to request Delta: N1 for a call

DK = 4#

Flag to request dK: -N2 for a call

GAMMA = 8#

Flag to request Gamma

LOGK = 64#

Flag to request logK, set to 1 where k=0 or vol*sqrtT=0

PRICE = 1#

Flag to request Price, for a call: N1 - k N2

THETA = 32#

Flag to request Theta, here defined as derivative in time-to-expiry (e.g. it is positive!).

VEGA = 16#

Flag to request Vega

__call__(k, vol, sqrtT=1.0, what=<BSFLAGS.PRICE: 1>, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes call option prices, and greeks in drift-less price domain efficiently.

This function aims to support almost any broadcast combination for its inputs.

The function returns values for the intrinsic call/put functions when strikes or sqrt-variances approach zero.

Example of using broadcastable shapes:

from cdxcore.bs import bs, np
sqrtT = np.array( [0.2, 0.4] ).reshape((1,2))
nms   = np.linspace( -1,+1,11 ).reshape((11,1))
k     = np.exp( nms * sqrtT )
v     = np.random.normal( size=(11,1) )**2
sqrtT = np.array( [0.2, 0.4] ).reshape((1,2))

price, vega = bs( k=k, vol=v, sqrtT=sqrtT, what=bs.PRICE|bs.VEGA )['price','vega']         
assert price.shape == (11,2)
Parameters:
knp.ndarray | float

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

whatcombination of cdxcore.bs.BSFLAGS flags, default BS.PRICE

A bitmask indicating what to compute. Can be any combination of:

  • BS.PRICE : Call price

  • BS.DELTA : Delta

  • BS.DK : Derivative in strike

  • BS.VEGA : Vega

  • BS.GAMMA : Gamma

  • BS.THETA : Theta as derivative in time-to-expiry, e.g. it is positive.

  • BS.LOGK : Log-strike, with 1 whereever k==0. This is mainly useful to avoid recomputing log(k) multiple times.

Note that if only one item is requested the function returns a np.ndarray or float. Otherwise it will return a cdxcore.pretty.PrettyValueObject with the requested outputs as attributes. The following then works as expected:

from cdxcore.bs import bs
price, vega = bs( k=0.8, vol=0.2, sqrtT=1., what=bs.VEGA|bs.PRICE )['price','vega'] 

You can also access the result by tuple access. The order of the items is always “price”, “delta”, “dk”, “gamma”, “vega”, “theta”, “logK” whichever was requested; hence you can also do:

price, vega = bs( k=0.8, vol=0.2, sqrtT=1., what=bs.VEGA|bs.PRICE )

Note that as expected the | operand does not alter the order of the returned data.

logKnp.ndarray | float | None, default None

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally. The function will mask this array where k==0, hence logK can be left NaN in those locations.

on_precision_error: str, default ``raise``

What to do if any of the calculated values are outside their theoretical bounds by more than some tolerance. Can be one of "clip", "warn", or "raise". The result is clipped to its theoretical bounds regardless of this setting; "warn" additionally issues a warning and "raise" raises a FloatingPointError instead of clipping.

epsfloat | None, default None

An optional precision tolerance for the internal checks. If not provided, the default is bs._eps (1E-8). Note that the function will cap/floor the values of prices or greeks with the respective bounds even if the validity test passed. Hence, if eps is set to a large number, this will lead to floored/capped outputs. Note, however, that some bounds such as the maximum value of a call of spot are not tight.

Returns:
resultnp.ndarray | float | PrettyValueObject[str, np.ndarray|float]

If only one what was requested, this function returns a float or np.ndarray for whatever calculation was requested.

If several what were requested, this function returns a PrettyObject with the requested outputs as attributes, named and in order:

  • price

  • delta

  • dk

  • gamma

  • vega

  • theta (positive!)

  • logK

That means you can access the result as follows:

from cdxcore.bs import bs
price, vega = bs( k=0.8, vol=0.2, sqrtT=1., what=bs.PRICE|bs.VEGA )['price','vega'] 

or in order of “price”, “delta”, “dk”, “gamma”, “vega”, “theta”, “logK” (whenever requested) directly in tuple notation:

price, gamma, vega = bs( k=0.8, vol=0.2, sqrtT=1., what=bs.PRICE|bs.VEGA|bs.GAMMA )

The returned values are in “pure” space. You can conver them as follows:

  • cash_delta = df * pure_delta

  • cash_gamma = df * pure_gamma / fwd

  • cash_vega  = df * fwd * pure_vega

  • cash_theta = df * fwd * pure_theta

  • cash_dK    = df * pure_dK

Raises:
input errors: ValueError

If any of the inputs is invalid, e.g. negative strikes or volatilities, or if the “what” bitmask is zero or not a valid combination of flags.

precision errors: FloatingPointError

In case any of the calculated values are outside their theoretical bounds by more than some tolerance. The tolerance level is set by self._eps which can be changed for debugging; however please raise issues of this type to the authors.

delta(k, vol, sqrtT=1.0, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes option deltas in drift-less price domain.

Parameters:
knp.ndarray

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

is_callnp.ndarray | bool, default True

Whether to compute call (True) or put (False) prices.

logK: np.ndarray | float | None, default ``None``

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally.

on_precision_errorstr, default raise

Action to take on precision errors. Options are raise to raise an exception or warn to issue a warning.

epsfloat | None, default None

An optional precision tolerance for greek validation checks. If not provided, the default is bs._eps (1E-8).

Returns:
deltanp.ndarray

Black Scholes deltas

dk(k, vol, sqrtT=1.0, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes dk (derivative in k) in drift-less price domain.

Parameters:
knp.ndarray

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

is_callnp.ndarray | bool, default True

Whether to compute call (True) or put (False) prices.

logK: np.ndarray | float | None, default ``None``

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally.

on_precision_errorstr, default raise

Action to take on precision errors. Options are raise to raise an exception or warn to issue a warning.

epsfloat | None, default None

An optional precision tolerance for internal price validation checks. If not provided, the default is bs._eps (1E-8).

Returns:
dknp.ndarray

Black Scholes dk (derivative in k)

gamma(k, vol, sqrtT=1.0, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes option gamma in drift-less price domain.

Parameters:
knp.ndarray

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

is_callnp.ndarray | bool, default True

Whether to compute call (True) or put (False) prices.

logK: np.ndarray | float | None, default ``None``

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally.

on_precision_errorstr, default raise

Action to take on precision errors. Options are raise to raise an exception or warn to issue a warning.

epsfloat | None, default None

An optional precision tolerance for greek validation checks. If not provided, the default is bs._eps (1E-8).

Returns:
gammanp.ndarray

Black Scholes gammas

implied(k, prices, is_call=True, sqrtT=1.0, *, price_tol=1e-06, vol_min=0.01, vol_max=5.0, default_vol=0.0, mask=None, max_iters=100, eps=1e-10, min_vega=1e-12, ret_only_vols=True, on_exceed_bounds='warn', verbose=<cdxcore.verbose.Context object>)[source]#

Solve for implied volatilities using a robust bisection and Newton-Raphson hybrid method. This function is designed to be run for a large number of potentially unrelated options, for example for a time series of surfaces of options.

This routine:

  1. Assigns vol_max to every option whose price is at or above the option price implied by vol_max, and vol_min to every option whose price is at or below the option price implied by vol_min.

  2. Initializes the search at default_vol if it is an array, or otherwise using the closed-form approximation (20). If default_vol is a float, then it is only used where the approximation fails to yield a value.

  3. Run a hybrid bisection and Newton-Raphson method to find the implied volatilities for the remaining options until the maximum number of itersations, max_iters, is reached or the method has converged in the sense that:

    |price(vol) - market_price| < price_tol
    

By default the function just returns the implied volatilities, or their last best guesses. If ret_only_vols is set to False, then it returns a cdxcore.pretty.PrettyValueObject containing the fitted prices, a boolean area indicating issues, and an error statistics object in the following fields:

  • vols contains the fitted volatilities.

  • fits contains the the prices at the fitted volatilities.

  • prices contains the input prices at those values.

  • failed contains a boolean array indicating which fits did not converge.

  • max_iters: the input max_iters.

  • iters: iterations used.

  • max_err: maximum price error.

  • l1_err: average price error.

  • l2_err: quadratic average price error.

Parameters:
knp.ndarray

Strikes.

pricesnp.ndarray

Prices in the same shape as k.

is_callnp.ndarray | bool, default True

Boolean or boolean array of shape compatible with k.

sqrtTnp.ndarray | float, default 1

Square-root of time as float or array compatible with k.

price_tolnp.ndarray | float, default 1E-6

Price tolerance (typically a fraction of spreads) as float or array or array compatible with k. Must be at least 1E-8. A standard value is 0.1*spread.

vol_minfloat, default 0.01

Minimum volatility.

vol_maxfloat, default 5

Maximum volatility.

default_volnp.ndarray | float, default 0.

Default and initial volatility guess as float or array compatible with k. See description above. Also note that default_vol will be clipped to the range [vol_min, vol_max].

masknp.ndarray | None, default None

A numpy array boolean mask to indicate which options to process. The implied vol of options excluded by mask will be set to default_vol.

max_itersint, default 100

Maximum iterations. Usually the routine uses very few iterations.

epsfloat, default 1E-10

Used to decide whether strike or sqrtT are zero, and as the precision tolerance passed to the internal cdxcore.bs.BS.price() and cdxcore.bs.BS.vega() calls.

min_vegafloat, default 1E-12

Minimum vega for taking an update step.

ret_only_volsbool, default True

Return only vols.

on_exceed_boundserror | warn | quiet | None, default warn

What to do if the input data violate basic option price bounds such as intrinsic from below or unit/strike from above, respectively. None is equivalent to quiet.

verbosecdxcore.verbose.Context, default cdxcore.verbose.Context.quiet

For printing progress information.

Returns:
resultsnp.ndarray | PrettyValueObject

See above.

price(k, vol, sqrtT=1.0, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes option prices in drift-less price domain.

Parameters:
knp.ndarray

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

is_callnp.ndarray | bool, default True

Whether to compute call (True) or put (False) prices.

logK: np.ndarray | float | None, default ``None``

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally.

on_precision_errorstr, default raise

Action to take on precision errors. Options are raise to raise an exception or warn to issue a warning.

epsfloat | None, default None

An optional precision tolerance for internal price validation checks. If not provided, the default is bs._eps (1E-8).

Returns:
pricenp.ndarray

Black Scholes prices

static pure_to_cash(pure, *, fwd, df)[source]#

Converts dictionary of pure price and greeks into market price and greeks.

Assume Cp(T,k) = E[(X_T-k)^+] where X is a pure log-normal martingale. Let C(T,K) := DF F Cp(T, K/F) be the market call price.

Then: `python Delta     = dC/dK = DF Delta^p Gamma     = d^2C/dK^2 = DF Gamma^p / F Vega      = dC/dsigma = DF F Vega^p Opt_Theta = dC/dt = DF F Theta^p (this is optionality theta, excluding curve and carry) DK        = dC/dF = DF DK^p `

Note that this function computes “optionality theta” as the decay due to loss of optionality, and excludes the effect on a change in discount factor or forward.

Parameters:
pureMapping

A dictionary containing any of the greeks above.

fwdfloat

Forward price.

dffloat

Discount factor.

Returns:
mktcdxcore.pretty.PrettyValueObject

A dictionary with the same inputs as pure; if theta was present in pure, then this object will contain opt_theta.

theta(k, vol, sqrtT=1.0, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes option theta in drift-less price domain.

Parameters:
knp.ndarray

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

is_callnp.ndarray | bool, default True

Whether to compute call (True) or put (False) prices.

logK: np.ndarray | float | None, default ``None``

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally.

on_precision_errorstr, default raise

Action to take on precision errors. Options are raise to raise an exception or warn to issue a warning.

epsfloat | None, default None

An optional precision tolerance for greek validation checks. If not provided, the default is bs._eps (1E-8).

Returns:
thetanp.ndarray

Black Scholes thetas

vega(k, vol, sqrtT=1.0, is_call=True, *, logK=None, on_precision_error='raise', eps=None)[source]#

Compute Black Scholes option vega in drift-less price domain.

Parameters:
knp.ndarray

Strikes.

volnp.ndarray | float

Volatilities or a single volatility.

sqrtTnp.ndarray | float, default 1

Square-root of time.

is_callnp.ndarray | bool, default True

Whether to compute call (True) or put (False) prices.

logK: np.ndarray | float | None, default ``None``

An optional pre-computed log-strike. If provided, this is used instead of computing log(k) internally.

on_precision_errorstr, default raise

Action to take on precision errors. Options are raise to raise an exception or warn to issue a warning.

epsfloat | None, default None

An optional precision tolerance for greek validation checks. If not provided, the default is bs._eps (1E-8).

Returns:
veganp.ndarray

Black Scholes vegas

class cdxcore.bs.BSFLAGS(*values)[source]#

Bases: IntFlag

DELTA = 2#

Flag to request Delta: N1 for a call

DK = 4#

Flag to request dK: -N2 for a call

GAMMA = 8#

Flag to request Gamma

LOGK = 64#

Flag to request logK, set to 1 where k=0 or vol*sqrtT=0

PRICE = 1#

Flag to request Price, for a call: N1 - k N2

THETA = 32#

Flag to request Theta

VEGA = 16#

Flag to request Vega

cdxcore.bs.bs = <cdxcore.bs.BS object>#

Main instance of the cdxcore.bs.BS class.

This is synthatic sugar for being able to write:

from cdxcore.bs import bs
price, vega = bs( k, vol, sqrtT, is_call, what=BS.PRICE|BS.VEGA )

or:

gamma = bs.gamma( k, vol, sqrtT, is_call )