All

Have you tried to calculate derivatives using TensorFlow 2?

mgreco

13/02/2020

No Comments

We will learn how to implement a simple function using TensorFlow 2 and how to obtain the derivatives from it. We will implement a Black-Scholes model for pricing a call option and then we are going to obtain the greeks.

Matthias Groncki wrote a very interesting post about how to obtain the greeks of a pricing option using TensorFlow which inspired me to write this post. So, I took the same example and make some updates to use TensorFlow 2.

Requirements

Black-Scholes pricing formula

We are going to implement the Black-Scholes formula for pricing options. In this example, we focus on the call option.

Version 2 of TensorFlow has many enhancements, especially on the python API which makes it easier to write code than before.

@tf.function
def pricer_blackScholes(S0, strike, time_to_expiry, implied_vol, riskfree):
    """Prices call option.

    Parameters
    ----------
    S0 : float
        Spot price.
    strike : float
        Strike price.
    time_to_expiry : float
        Time to maturity.
    implied_vol : float
        Volatility.
    riskfree : float
        Risk free rate.

    Returns
    -------
    npv : float
        Net present value.

    Examples
    --------
    >>> kw = initialize_variables(to_tf=True)
    >>> pricer_blackScholes(**kw)

    Notes
    -----
    https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model#Black%E2%80%93Scholes_formula
    """
    S       = S0
    K       = strike
    dt      = time_to_expiry
    dt_sqrt = tf.sqrt(dt)
    sigma   = implied_vol
    r       = riskfree
    Phi     = tf.compat.v1.distributions.Normal(0., 1.).cdf

    d1 = (tf.math.log(S / K) + (r + sigma ** 2 / 2) * dt) / (sigma * dt_sqrt)
    d2 = d1 - sigma * dt_sqrt

    npv =  S * Phi(d1) - K * tf.exp(-r * dt) * Phi(d2)
    return npv

As we can see the above code is the implementation of a call option in terms of the Black-Scholes framework. A very cool improvement is the tf.function decorator which creates a callable graph for us.

Calculating Derivatives

In previous versions of TensorFlow, we need to use tf.gradient which requires us to create a session and plenty of annoying stuff. Now, all this process is done using tf.GradientTape which is simpler. We may get it done writing something like :

with tf.GradientTape() as g1:
    npv = pricer_blackScholes(**variables)
dv = g1.gradient(npv, variables)  # first order derivatives

Ok, but what if we want higher-order derivatives? The answer is easy, we only have to add a new tf.GradientTape:

with tf.GradientTape() as g2:
    with tf.GradientTape() as g1:
        npv = pricer_blackScholes(**variables)
    dv = g1.gradient(npv, variables)
d2v = g2.gradient(dv, variables)

Black-Scholes model

We use the well-known Black-Scholes model to estimate the price of the call. Our code can be written as follows:

@tf.function
def pricer_blackScholes(S0, strike, time_to_expiry, implied_vol, riskfree):
    """pricer_blackScholes.

    Parameters
    ----------
    S0 : tensorflow.Variable
        Underlying spot price.
    strike : tensorflow.Variable
        Strike price.
    time_to_expiry : tensorflow.Variable
        Time to expiry.
    implied_vol : tensorflow.Variable
        Volatility.
    riskfree : tensorflow.Variable
        Risk free rate.

    Returns
    -------
    npv : tensorflow.Tensor
        Net present value.

    Examples
    --------
    >>> kw = initialize_variables(to_tf=True)
    >>> pricer_blackScholes(**kw)
    <tf.Tensor: id=120, shape=(), dtype=float32, numpy=9.739834>

    Notes
    -----
    Formula: https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model#Black%E2%80%93Scholes_formula
    """
    S       = S0
    K       = strike
    dt      = time_to_expiry
    dt_sqrt = tf.sqrt(dt)
    sigma   = implied_vol
    r       = riskfree
    Phi     = tf.compat.v1.distributions.Normal(0., 1.).cdf

    d1 = (tf.math.log(S / K) + (r + sigma ** 2 / 2) * dt) / (sigma * dt_sqrt)
    d2 = d1 - sigma * dt_sqrt

    npv =  S * Phi(d1) - K * tf.exp(-r * dt) * Phi(d2)
    return npv

To get the net present value (NPV) and the greeks (derivatives) we can write a function that wraps all the process. It’s optional of course but very useful.

def calculate_blackScholes():
    """calculate_blackScholes.

    Returns
    -------
    out : dict
        npv : net present value
        dv : First order derivates

    Examples
    --------
    >>> out = calculate_blackScholes()
    >>> pprint(out)
    {'dv': {'S0': 0.5066145,
            'implied_vol': 56.411205,
            'riskfree': 81.843216,
            'strike': -0.37201464,
            'time_to_expiry': 4.0482087},
     'npv': 9.739834}
    """
    variables = initialize_variables(to_tf=True)

    with tf.GradientTape() as g1:
        npv = pricer_blackScholes(**variables)
    dv = g1.gradient(npv, variables)

    dv = {k: v.numpy() for k,v in dv.items()}  # get the value
    return dict(npv=npv.numpy(), dv=dv)

The previous function returns:

>>> calculate_blackScholes()
{'dv': {'S0': 0.5066145,
        'implied_vol': 56.411205,
        'riskfree': 81.843216,
        'strike': -0.37201464,
        'time_to_expiry': 4.048208},
 'npv': 9.739834}

Where:

  • npv : The net present value is 9.74.
  • S0 = $$\frac{\partial v}{\partial S}$$
  • implied_vol = $$\frac{\partial v}{\partial \sigma}$$
  • strike = $$\frac{\partial v}{\partial K}$$
  • time_to_expiry = $$\frac{\partial v}{\partial \tau}$$

We have seen how to implement a TensorFlow function and how to get the derivatives from it. Now, we are going to see another example using the Monte Carlo method.

Monte Carlo method

The Monte Carlo method is very useful when we don’t have a closed formula or it’s very complex. We are going to implement the Monte Carlo pricing function, for this task I decided to implement a brownian function too, which is used inside of pricer_montecarlo.

@tf.function
def pricer_montecarlo(S0, strike, time_to_expiry, implied_vol, riskfree, dw):
    """Monte Carlo pricing method.

    Parameters
    ----------
    S0 : tensorflow.Variable
        Underlying spot price.
    strike : tensorflow.Variable
        Strike price.
    time_to_expiry : tensorflow.Variable
        Time to expiry.
    implied_vol : tensorflow.Variable
        Volatility.
    riskfree : tensorflow.Variable
        Risk free rate.
    dw : tensorflow.Variable
        Normal random variable.

    Returns
    -------
    npv : tensorflow.Variable
        Net present value.

    Examples
    --------
    >>> nsims = 10
    >>> nobs = 100
    >>> dw = tf.random.normal((nsims, nobs), seed=3232)
    >>> v = initialize_variables(to_tf=True)
    >>> npv = pricer_montecarlo(**v, dw=dw)
    >>> npv
    <tf.Tensor: id=646, shape=(), dtype=float32, numpy=28.780073>
    """
    sigma = implied_vol
    T = time_to_expiry
    r = riskfree
    K = strike
    dt = T / dw.shape[1]

    st = brownian(S0, dt, sigma, r, dw)
    payout = tf.math.maximum(st[:, -1] - K, 0)
    npv = tf.exp(-r * T) * tf.reduce_mean(payout)

    return npv


@tf.function
def brownian(S0, dt, sigma, mu, dw):
    """Generates a brownian motion.

    Parameters
    ----------
    S0 : tensorflow.Variable
        Initial value of Spot.
    dt : tensorflow.Variable
        Time step.
    sigma : tensorflow.Variable
        Volatility.
    mu : tensorflow.Variable
        Mean, in black Scholes frame it's the risk free rate.
    dw : tensorflow.Variable
        Random variable.

    Returns
    -------
    out : numpy.array

    Examples
    --------
    >>> nsims = 10
    >>> nobs = 400
    >>> v = initialize_variables(to_tf=True)
    >>> S0 = v["S0"]
    >>> dw = tf.random.normal((nsims, nobs), seed=SEED)
    >>> dt = v["time_to_expiry"] / dw.shape[1]
    >>> sigma = v["implied_vol"]
    >>> r = v["riskfree"]
    >>> paths = np.transpose(brownian(S0, dt, sigma, r, dw))
    """
    dt_sqrt = tf.math.sqrt(dt)
    shock = sigma * dt_sqrt * dw
    drift = (mu - (sigma ** 2) / 2)
    bm = tf.math.exp(drift * dt + shock)
    out = S0 * tf.math.cumprod(bm, axis=1)
    return out

Now, we are ready to calculate the NPV and the greeks under this frame.

def calculate_montecarlo(greeks=True):
    """calculate_montecarlo.

    Returns
    -------
    out : dict
        npv : Net present value
        dv : First order derivatives
        d2v : Second order derivatives

    Examples
    --------
    >>> out = calculate_montecarlo()
    >>> pprint(out)
    {'dv': {'S0': 0.5065364,
            'implied_vol': 56.45906,
            'riskfree': 81.81441,
            'strike': -0.37188327,
            'time_to_expiry': 4.050169},
     'npv': 9.746445}
    """
    nsims = 10000000
    nobs = 2
    dw = tf.random.normal((nsims, nobs), seed=SEED)
    v = initialize_variables(to_tf=True)

    out = dict()

    with tf.GradientTape() as g1:
        npv = pricer_montecarlo(**v, dw=dw).numpy()
    dv = g1.gradient(npv, v)

    out["dv"] = {k: v.numpy() for k, v in dv.items()}
    return out

The output:

>>> out = calculate_montecarlo()
>>> pprint(out)
{'dv': {'S0': 0.5065364,
        'implied_vol': 56.45906,
        'riskfree': 81.81441,
        'strike': -0.37188327,
        'time_to_expiry': 4.050169},
 'npv': 9.746445}

Comparison

We are taking a look at the results of both methods:

VariableBlack-ScholesMontecarlo
npv9.7464459.739834
$$\frac{\partial v}{\partial S}$$0.50653640.5066145
$$\frac{\partial v}{\partial \sigma}$$56.4590656.411205
$$\frac{\partial v}{\partial r}$$81.8144181.843216
$$\frac{\partial v}{\partial K}$$-0.37188327-0.37201464
$$\frac{\partial v}{\partial \tau}$$4.0501694.048208

As we can see, we can get similar results with both methods. There is room for improvement, for example, we can increase the number of simulations into the Monte Carlo method. However, the results are reasonably close. Notice that the new version of TensorFlow makes the development process pretty simple which is great news for quants.

Have you ever used this before? It would be great if you can tell your use case in the comments.

Keep coding!