Basic principles of pricing and hedging

We shall first go through basic principles of modeling in mathematical Finance before we introduce concepts of interest rate theory.

We have seen in one step bi- and tri-nomial models that pricing and hedging are in a fundamental duality relationship given a certain payoff: the largest arbitrage free price equals the smallest price of a super-hedging portfolio.

Prices are free of arbitrage if their introduction into the market does not lead to arbitrages.

A super-hedging portfolio is a self-financing portfolio (i.e. the value process is the initial value of the portfolio plus the P&L process -- all in discounted terms) dominating a certain payoff.

In case that there is only one arbitrage-free price for a payoff $X$ the super-hedging portfolio actually is a hedging portfolio leading to the remarkable relationship (in discounted terms) $$ X = \text{ price } + \text{ P & L } \, . $$ Furthermore models are free of arbitrage if and only if there exists an equivalent pricing measure, which associates in particular to P & L processes the value $0$. Whence prices of payoffs can be calculated by taking expectations (i.e. a Monte Carlo evaluation is possible) with respect to this equivalent pricing measure.

Next we shall see the concepts of pricing and hedging realized in a geometric Brownian motion market environment. We shall use code from https://github.com/yhilpisch/dx for this purpose.

In [1]:
import dx
import datetime as dt
import pandas as pd
import seaborn as sns; sns.set()
import numpy as np
/scratch/users/jteichma/.local/lib/python3.6/site-packages/statsmodels/compat/pandas.py:56: FutureWarning: The pandas.core.datetools module is deprecated and will be removed in a future version. Please use the pandas.tseries module instead.
  from pandas.core import datetools

First we shall define some market environment:

In [2]:
r = dx.constant_short_rate('r', 0.01)
  # a constant short rate

cas_1 = dx.market_environment('cas', dt.datetime(2016, 1, 1))
    
cas_1.add_constant('initial_value', 100.)
  # starting value of simulated processes
cas_1.add_constant('volatility', 0.2)
  # volatiltiy factor
cas_1.add_constant('final_date', dt.datetime(2017, 1, 1))
  # horizon for simulation
cas_1.add_constant('currency', 'EUR')
  # currency of instrument
cas_1.add_constant('frequency', 'D')
  # frequency for discretization
cas_1.add_constant('paths', 10000)
  # number of paths
cas_1.add_curve('discount_curve', r)
  # number of paths

Let us introduce a geometric Brownian motion in the above market environment.

In [3]:
gbm_cas_1 = dx.geometric_brownian_motion('gbm_1', cas_1)

We can obtain one trajectory generated on the predefined weekly time grid of GBM.

In [4]:
paths_gbm_cas_1 = pd.DataFrame(gbm_cas_1.get_instrument_values(), index=gbm_cas_1.time_grid)
In [5]:
%matplotlib inline
paths_gbm_cas_1.loc[:, :10].plot(legend=False, figsize=(10, 6))
Out[5]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fa3c6549fd0>

Next we define a new market enviroment which inherits from the previous one but has an additional Europen call option.

In [6]:
cas_1_opt = dx.market_environment('cas_1_opt', cas_1.pricing_date)
cas_1_opt.add_environment(cas_1)
cas_1_opt.add_constant('maturity', dt.datetime(2017, 1, 1))
cas_1_opt.add_constant('strike', 110.)
In [7]:
eur_call = dx.valuation_mcs_european_single(
            name='eur_call',
            underlying=gbm_cas_1,
            mar_env=cas_1_opt,
            payoff_func='np.maximum(maturity_value - strike, 0)')

The present value of the European call is uniquely given by no arbitrage arguments.

In [8]:
eur_call.present_value()
Out[8]:
4.5594169999999998

The delta is the sensitivity with respect to the initial value.

In [9]:
eur_call.delta()
Out[9]:
0.37859999999999999

The gamma is the sensitivity of the delta with respect to the initial value.

In [10]:
eur_call.gamma()
Out[10]:
0.019699999999999999

The vega is the sensitivity with respect to volatility.

In [11]:
eur_call.vega()
Out[11]:
37.525100000000002

The theta is sensitivity with respect to maturity.

In [12]:
eur_call.theta()
Out[12]:
-4.2329999999999997

The rho is sensitivity with respect to interest rate.

In [13]:
eur_call.rho()
Out[13]:
33.1355

The previous quantities allow to understand the risks of the given derivative from the point of view of the chosen model in terms of sensitivities.

In the sequel we demonstrate the precise meaning of the option's delta by means of a running hedging portfolio.

In [14]:
path = gbm_cas_1.get_instrument_values()[:,0]
timegrid = gbm_cas_1.time_grid
presentvalue = eur_call.present_value()
n = len(path)
pnl = [presentvalue]
optionvalue = [np.maximum(0,path[0]-eur_call.strike)]
In [15]:
for i in range(n-1):
    r = dx.constant_short_rate('r', 0.01)
    # a constant short rate

    running = dx.market_environment('running', timegrid[i])
    
    running.add_constant('initial_value', path[i])
    # starting value of simulated processes
    running.add_constant('volatility', 0.2)
    # volatiltiy factor
    running.add_constant('final_date', dt.datetime(2017, 1, 1))
    # horizon for simulation
    running.add_constant('currency', 'EUR')
    # currency of instrument
    running.add_constant('frequency', 'W')
    # frequency for discretization
    running.add_constant('paths', 10000)
    # number of paths
    running.add_curve('discount_curve', r)
    # number of paths
    gbm_running = dx.geometric_brownian_motion('gbm_running', running)
    opt_running = dx.market_environment('opt_running', running.pricing_date)
    opt_running.add_environment(running)
    opt_running.add_constant('maturity', dt.datetime(2017, 1, 1))
    opt_running.add_constant('strike', 110.)
    eur_call = dx.valuation_mcs_european_single(
            name='eur_call',
            underlying=gbm_running,
            mar_env=opt_running,
            payoff_func='np.maximum(maturity_value - strike, 0)')
    #print(path[i])
    #print(timegrid[i])
    #print(eur_call.delta())
    pnl = pnl + [pnl[-1]+eur_call.delta()*(path[i+1]-path[i])]
    optionvalue = optionvalue + [np.maximum(0,path[i+1]-eur_call.strike)]
In [16]:
data = []
for j in range(n):
    data = data + [[optionvalue[j],pnl[j]]]
In [17]:
paths_hedge = pd.DataFrame(data, index=gbm_cas_1.time_grid)
%matplotlib inline
paths_hedge.loc[:,:].plot(legend=True, figsize=(10, 6))
Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fa3c63b6c88>

Interest rate terminology

Prices of zero-coupon bonds (ZCB) with maturity $ T $ are denoted by $P(t,T)$. Interest rates are governed by a market of (default free) zero-coupon bonds modeled by stochastic processes $ {(P(t,T))}_{0 \leq t \leq T} $ for $ T \geq 0 $. We assume the normalization $ P(T,T)=1 $.

$T$ denotes the maturity of the bond, $P(t,T)$ its price at a time $t$ before maturity $T$.

The yield $$ Y(t,T) = - \frac{1}{T-t} \log P(t,T) $$ describes the compound interest rate p.a. for maturity $T$.

The curve $f$ is called the forward rate curve of the bond market \begin{align*} P(t,T) & =\exp(-\int_{t}^{T}f(t,s)ds) \end{align*} for $0\leq t\leq T$.

Let us understand a bit the dynamics of yield curves and forward rate curves at this point:

In [ ]:
from mpl_toolkits.mplot3d import Axes3D
import copy as copylib
from progressbar import *
%pylab
%matplotlib inline
import pandas as pandas
pylab.rcParams['figure.figsize'] = (16, 4.5)
numpy.random.seed(0)

First we load some historical data:

In [ ]:
dataframe =  pandas.DataFrame.from_csv('hjm_data.csv')
dataframe = dataframe / 100 # Convert interest rates to %
pandas.options.display.max_rows = 10
display(dataframe)
In [7]:
hist_timeline = list(dataframe.index)
tenors = [float(x) for x in dataframe.columns]
hist_rates = matrix(dataframe)
plot(hist_rates), xlabel(r'Time $t$'), title(r'Historical $f(t,\tau)$ by $t$'), show()
plot(tenors, hist_rates.transpose()), xlabel(r'Tenor $\tau$'), title(r'Historical $f(t,\tau)$ by $\tau$');