Model Classes

The model classes represent the fundamental building blocks to model a financial market. They are used to represent the fundamental risk factors driving uncertainty (e.g. a stock, an equity index an interest rate). The following models are available:

  • geometric_brownian_motion: Black-Scholes-Merton (1973) geometric Brownian motion
  • jump_diffusion: Merton (1976) jump diffusion
  • stochastic_volatility: Heston (1993) stochastic volatility model
  • stoch_vol_jump_diffusion: Bates (1996) stochastic volatility jump diffusion
  • square_root_diffusion: Cox-Ingersoll-Ross (1985) square-root diffusion
  • square_root_jump_diffusion: square-root jump diffusion (experimental)
  • square_root_jump_diffusion_plus: square-root jump diffusion plus term structure (experimental)

In [1]:
from dx import *

In [2]:
np.set_printoptions(precision=3)

Throughout this section we fix a constant_short_rate discounting object.


In [3]:
r = constant_short_rate('r', 0.06)

geometric_brownian_motion

To instantiate any kind of model class, you need a market_environment object conataining a minimum set of data (depending on the specific model class).


In [4]:
me = market_environment(name='me', pricing_date=dt.datetime(2015, 1, 1))

For the geometric Browniam motion class, the minimum set is as follows with regard to the constant parameter values. Here, we simply make assumptions, in practice the single values would, for example be retrieved from a data service provider like Thomson Reuters or Bloomberg. The frequency parameter is according to the pandas frequency conventions (cf. http://pandas.pydata.org/pandas-docs/stable/timeseries.html).


In [5]:
me.add_constant('initial_value', 36.)
me.add_constant('volatility', 0.2)
me.add_constant('final_date', dt.datetime(2015, 12, 31))
  # time horizon for the simulation
me.add_constant('currency', 'EUR')
me.add_constant('frequency', 'M')
  # monthly frequency; paramter accorind to pandas convention
me.add_constant('paths', 10000)
  # number of paths for simulation

Every model class needs a discounting object since this defines the risk-neutral drift of the risk factor.


In [6]:
me.add_curve('discount_curve', r)

The instantiation of a model class is then accomplished by providing a name as a string object and the respective market_environment object.


In [7]:
gbm = geometric_brownian_motion('gbm', me)

The generate_time_grid method generates a ndarray objet of datetime objects given the specifications in the market environment. This represents the discretization of the time interval between the pricing_date and the final_date. This method does not need to be called actively.


In [8]:
gbm.generate_time_grid()

In [9]:
gbm.time_grid


Out[9]:
array([datetime.datetime(2015, 1, 1, 0, 0),
       datetime.datetime(2015, 1, 31, 0, 0),
       datetime.datetime(2015, 2, 28, 0, 0),
       datetime.datetime(2015, 3, 31, 0, 0),
       datetime.datetime(2015, 4, 30, 0, 0),
       datetime.datetime(2015, 5, 31, 0, 0),
       datetime.datetime(2015, 6, 30, 0, 0),
       datetime.datetime(2015, 7, 31, 0, 0),
       datetime.datetime(2015, 8, 31, 0, 0),
       datetime.datetime(2015, 9, 30, 0, 0),
       datetime.datetime(2015, 10, 31, 0, 0),
       datetime.datetime(2015, 11, 30, 0, 0),
       datetime.datetime(2015, 12, 31, 0, 0)], dtype=object)

The simulation itself is initiated by a call of the method get_instrument_values. It returns an ndarray object containing the simulated paths for the risk factor.


In [10]:
paths = gbm.get_instrument_values()

In [11]:
paths[:, :2]


Out[11]:
array([[ 36.   ,  36.   ],
       [ 37.403,  38.12 ],
       [ 39.521,  42.255],
       [ 42.618,  35.909],
       [ 41.901,  35.88 ],
       [ 40.316,  31.446],
       [ 41.314,  31.707],
       [ 39.202,  31.282],
       [ 40.99 ,  30.124],
       [ 40.868,  31.473],
       [ 40.492,  33.443],
       [ 42.483,  36.925],
       [ 43.766,  37.805]])

These can, for instance, be visualized easily. First some plotting parameter specifications we want to use throughout.


In [12]:
%matplotlib inline
colormap='RdYlBu_r'
lw=1.25
figsize=(8, 4)
legend=False
no_paths=10

For easy plotting, we put the data with the time_grid information into a pandas DataFrame object.


In [13]:
pdf = pd.DataFrame(paths, index=gbm.time_grid)

The following visualizes the first 10 paths of the simulation.


In [14]:
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x109a0b790>

jump_diffusion

The next model is the jump diffusion model from Merton (1976) adding a log-normally distributed jump component to the geometric Brownian motion. Three more parameter values are needed:


In [15]:
me.add_constant('lambda', 0.7)
  # probability for a jump p.a.
me.add_constant('mu', -0.8)
  # expected relative jump size
me.add_constant('delta', 0.1)
  # standard deviation of relative jump

The instantiation of the model class and usage then is the same as before.


In [16]:
jd = jump_diffusion('jd', me)

In [17]:
paths = jd.get_instrument_values()

In [18]:
pdf = pd.DataFrame(paths, index=jd.time_grid)

In [19]:
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[19]:
<matplotlib.axes._subplots.AxesSubplot at 0x109de3890>

stochastic_volatility

Another important financial model is the stochastic volatility model according to Heston (1993). Compared to the geometric Brownian motion, this model class need four more parameter values.


In [20]:
me.add_constant('rho', -.5)
  # correlation between risk factor process (eg index)
  # and variance process 
me.add_constant('kappa', 2.5)
  # mean reversion factor
me.add_constant('theta', 0.1)
  # long-term variance level
me.add_constant('vol_vol', 0.1)
  # volatility factor for variance process

Again, the instantiation and usage of this model class are essentially the same.


In [21]:
sv = stochastic_volatility('sv', me)

The following visualizes 10 simulated paths for the risk factor process.


In [22]:
paths = sv.get_instrument_values()

In [23]:
pdf = pd.DataFrame(paths, index=sv.time_grid)

In [24]:
# index level paths
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[24]:
<matplotlib.axes._subplots.AxesSubplot at 0x10a9717d0>

This model class has a second set of simulated paths, namely for the variance process.


In [25]:
vols = sv.get_volatility_values()

In [26]:
pdf = pd.DataFrame(vols, index=sv.time_grid)

In [27]:
# volatility paths
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[27]:
<matplotlib.axes._subplots.AxesSubplot at 0x10abccb50>

stoch_vol_jump_diffusion

The next model class, i.e. stoch_vol_jump_diffusion, combines stochastic volatility with a jump diffusion according to Bates (1996). Our market environment object me contains already all parameters needed for the instantiation of this model.


In [28]:
svjd = stoch_vol_jump_diffusion('svjd', me)

As with the stochastic_volatility class, this class generates simulated paths for both the risk factor and variance process.


In [29]:
paths = svjd.get_instrument_values()

In [30]:
pdf = pd.DataFrame(paths, index=svjd.time_grid)

In [31]:
# index level paths
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[31]:
<matplotlib.axes._subplots.AxesSubplot at 0x10b15f910>

In [32]:
vols = svjd.get_volatility_values()

In [33]:
pdf = pd.DataFrame(vols, index=svjd.time_grid)

In [34]:
# volatility paths
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[34]:
<matplotlib.axes._subplots.AxesSubplot at 0x10b631450>

square_root_diffusion

The square_root_diffusion model class is based on the square-root diffusion according to Cox-Ingersoll-Ross (1985). This class is often used to model stochastic short rates or a volatility process (eg like the VSTOXX volatility index). The model needs the following set of parameters:


In [35]:
# short rate like parameters
me.add_constant('initial_value', 0.05)
  # starting value (eg inital short rate)
me.add_constant('volatility', 0.1)
  # volatility factor
me.add_constant('kappa', 2.5)
  # mean reversion factor
me.add_constant('theta', 0.01)
  # long-term mean

As before, the handling of the model class is the same, making it easy to simulate paths given the parameter specifications and visualize them.


In [36]:
srd = square_root_diffusion('srd', me)

In [37]:
paths = srd.get_instrument_values()

In [38]:
pdf = pd.DataFrame(paths, index=srd.time_grid)

In [39]:
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[39]:
<matplotlib.axes._subplots.AxesSubplot at 0x10b8da990>

square_root_jump_diffusion

Experimental Status

Building on the square-root diffusion, there is a square-root jump diffusion adding a log-normally distributed jump component. The following parmeters might be for a volatility index, for example. In this case, the major risk might be a large positive jump in the index. The following model parameters are needed:


In [40]:
# volatility index like parameters
me.add_constant('initial_value', 25.)
  # starting values
me.add_constant('kappa', 2.)
  # mean-reversion factor
me.add_constant('theta', 20.)
  # long-term mean
me.add_constant('volatility', 1.)
  # volatility of diffusion
me.add_constant('lambda', 0.3)
  # probability for jump p.a.
me.add_constant('mu', 0.4)
  # expected jump size
me.add_constant('delta', 0.2)
  # standard deviation of jump

Once the square_root_jump_diffusion class is instantiated, the handling of the resulting object is the same as with the other model classes.


In [41]:
srjd = square_root_jump_diffusion('srjd', me)

In [42]:
paths = srjd.get_instrument_values()

In [43]:
pdf = pd.DataFrame(paths, index=srjd.time_grid)

In [44]:
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[44]:
<matplotlib.axes._subplots.AxesSubplot at 0x10ba5b790>

In [45]:
paths[-1].mean()


Out[45]:
22.138789232066785

square_root_jump_diffusion_plus

Experimental Status

This model class further enhances the square_root_jump_diffusion class by adding a deterministic shift approach according to Brigo-Mercurio (2001) to account for a market given term structure (e.g. in volatility, interest rates). Let us define a simple term structure as follows:


In [46]:
term_structure = np.array([(dt.datetime(2015, 1, 1), 25.),
                  (dt.datetime(2015, 3, 31), 24.),
                  (dt.datetime(2015, 6, 30), 27.),
                  (dt.datetime(2015, 9, 30), 28.),
                  (dt.datetime(2015, 12, 31), 30.)])

In [47]:
me.add_curve('term_structure', term_structure)

In [48]:
srjdp = square_root_jump_diffusion_plus('srjdp', me)

The method generate_shift_base calibrates the square-root diffusion to the given term structure (varying the parameters kappa, theta and volatility).


In [49]:
srjdp.generate_shift_base((2.0, 20., 0.1))


Optimization terminated successfully.
         Current function value: 1.117824
         Iterations: 229
         Function evaluations: 431

The results are shift_base values, i.e. the difference between the model and market implied foward rates.


In [50]:
srjdp.shift_base
  # difference between market and model
  # forward rates after calibration


Out[50]:
array([[datetime.datetime(2015, 1, 1, 0, 0), 0.0],
       [datetime.datetime(2015, 3, 31, 0, 0), -2.139861715691705],
       [datetime.datetime(2015, 6, 30, 0, 0), -0.19434592701965414],
       [datetime.datetime(2015, 9, 30, 0, 0), -0.157275574314653],
       [datetime.datetime(2015, 12, 31, 0, 0), 0.9734498133023948]], dtype=object)

The method update_shift_values then calculates deterministic shift values for the relevant time grid by interpolation of the shift_base values.


In [51]:
srjdp.update_shift_values()

In [52]:
srjdp.shift_values
  # shift values to apply to simulation scheme
  # given the shift base values


Out[52]:
array([[datetime.datetime(2015, 1, 1, 0, 0), 0.0],
       [datetime.datetime(2015, 1, 31, 0, 0), -0.72130170191855247],
       [datetime.datetime(2015, 2, 28, 0, 0), -1.3945166237092015],
       [datetime.datetime(2015, 3, 31, 0, 0), -2.1398617156917048],
       [datetime.datetime(2015, 4, 30, 0, 0), -1.4984828842613584],
       [datetime.datetime(2015, 5, 31, 0, 0), -0.8357247584500006],
       [datetime.datetime(2015, 6, 30, 0, 0), -0.19434592701965414],
       [datetime.datetime(2015, 7, 31, 0, 0), -0.18185482991253418],
       [datetime.datetime(2015, 8, 31, 0, 0), -0.16936373280541422],
       [datetime.datetime(2015, 9, 30, 0, 0), -0.157275574314653],
       [datetime.datetime(2015, 10, 31, 0, 0), 0.22372971933891742],
       [datetime.datetime(2015, 11, 30, 0, 0), 0.59244451964882439],
       [datetime.datetime(2015, 12, 31, 0, 0), 0.97344981330239477]], dtype=object)

When simulating the process, the model forward rates ...


In [53]:
srjdp.update_forward_rates()
srjdp.forward_rates
  # model forward rates resulting from parameters


Out[53]:
array([[datetime.datetime(2015, 1, 1, 0, 0), 25.0],
       [datetime.datetime(2015, 1, 31, 0, 0), 22.65694757330202],
       [datetime.datetime(2015, 2, 28, 0, 0), 20.700760393345639],
       [datetime.datetime(2015, 3, 31, 0, 0), 18.797095034394516],
       [datetime.datetime(2015, 4, 30, 0, 0), 17.206256506214984],
       [datetime.datetime(2015, 5, 31, 0, 0), 15.802066864566269],
       [datetime.datetime(2015, 6, 30, 0, 0), 14.651607085467674],
       [datetime.datetime(2015, 7, 31, 0, 0), 13.652189090303105],
       [datetime.datetime(2015, 8, 31, 0, 0), 12.819382165050907],
       [datetime.datetime(2015, 9, 30, 0, 0), 12.149056828894659],
       [datetime.datetime(2015, 10, 31, 0, 0), 11.575039304260786],
       [datetime.datetime(2015, 11, 30, 0, 0), 11.116201461381866],
       [datetime.datetime(2015, 12, 31, 0, 0), 10.725486593397667]], dtype=object)

... are then shifted by the shift_values to better match the term structure.


In [54]:
srjdp.forward_rates[:, 1] + srjdp.shift_values[:, 1]
  # shifted foward rates


Out[54]:
array([25.0, 21.935645871383468, 19.306243769636438, 16.657233318702811,
       15.707773621953626, 14.966342106116269, 14.45726115844802,
       13.470334260390571, 12.650018432245494, 11.991781254580006,
       11.798769023599704, 11.70864598103069, 11.698936406700062], dtype=object)

The simulated paths then are including the deterministic shift.


In [55]:
paths = srjdp.get_instrument_values()

In [56]:
pdf = pd.DataFrame(paths, index=srjdp.time_grid)

In [57]:
pdf[pdf.columns[:no_paths]].plot(colormap=colormap, lw=lw,
                           figsize=figsize, legend=legend)


Out[57]:
<matplotlib.axes._subplots.AxesSubplot at 0x10bd14990>

The effect might not be immediately visible in the paths plot, however, the mean of the simulated values in this case is higher by about 1 point compared to the square_root_jump_diffusion simulation without deterministic shift.


In [58]:
paths[-1].mean()


Out[58]:
23.112239045369179

Copyright, License & Disclaimer

© Dr. Yves J. Hilpisch | The Python Quants GmbH

DX Analytics (the "dx library") is licensed under the GNU Affero General Public License version 3 or later (see http://www.gnu.org/licenses/).

DX Analytics comes with no representations or warranties, to the extent permitted by applicable law.


http://www.pythonquants.com | analytics@pythonquants.com | http://twitter.com/dyjh

Python Quant Platform | http://quant-platform.com

Derivatives Analytics with Python (Wiley Finance) | http://derivatives-analytics-with-python.com

Python for Finance (O'Reilly) | http://shop.oreilly.com/product/0636920032441.do