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.
[mathjax]
We are going to implement the Black-Scholes formula for pricing options. In this example we focus on the call option.
The version 2 of TensorFlow has many enhancements, especially on the python API which makes it** **easier to write code than before.
::: {#cb1 .sourceCode}
@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.
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 :
::: {#cb2 .sourceCode}
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
:
::: {#cb3 .sourceCode}
with tf.GradientTape() as g2:
with tf.GradientTape() as g1:
npv = pricer_blackScholes(**variables)
dv = g1.gradient(npv, variables)
d2v = g2.gradient(dv, variables)
:::
We use the well-known Black-Scholes model to estimate the price of the call. Our code can be written as follows:
::: {#cb4 .sourceCode}
@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.
::: {#cb5 .sourceCode}
@tf.function
def pricer_blackScholes(S0, strike, time_to_expiry, implied_vol, riskfree):
"""Calculates NPV and greeks using Black-Scholes model.
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
:::
The previous function returns:
::: {#cb6 .sourceCode}
>>> 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}$$]{.math .inline}implied_vol
= [$$\frac{\partial v}{\partial \sigma}$$]{.math .inline}strike
= [$$\frac{\partial v}{\partial K}$$]{.math .inline}time_to_expiry
= [$$\frac{\partial v}{\partial \tau}$$]{.math .inline}
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.
The Monte Carlo method is very useful when we don't have the 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
.
::: {#cb7 .sourceCode}
@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.
::: {#cb8 .sourceCode}
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:
::: {#cb9 .sourceCode}
>>> 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}
:::
We are taking a look at the results of both methods:
Black-Scholes Montecarlo
npv 9.746445 9.739834
[\$\$\\frac{\\partial v}{\\partial S}\$\$]{.math .inline} 0.5065364 0.5066145
[$$\frac{\partial v}{\partial \sigma}$$]{.math .inline} 56.45906 56.411205 [$$\frac{\partial v}{\partial r}$$]{.math .inline} 81.81441 81.843216 [$$\frac{\partial v}{\partial K}$$]{.math .inline} -0.37188327 -0.37201464 [$$\frac{\partial v}{\partial \tau}$$]{.math .inline} 4.050169 4.048208
As we can see, we can get similar results with both methods. There is room for improvements, for example: We can increase the number of simulations into Monte Carlo method. However, results are reasonably close. Notice that the new version of TensorFlow makes the development process pretty simple which is a great news for quants.
Have you used this before? would be great if you can tell your use case in the comments.
Keep coding!