Object Orientated Programming and Simulating Derivatives in a Fincanial Environment - Scalable and Dynamic Programming using the Power of Python

  • The power of object orientated programming comes from its ability to store information about an object and to also carry out functions upon it!

  • Simulating derivatives requires a host of parameters: simulation parameters, such as the number of paths, the characteristsics of the underlying intstrument, the interest rate environment, the currencies involved etc etc. Then there are a host of different models that can be chosen to carry out the stochatistic projections of the underlying instrument, the variable payoff functions of the derivaties themselves and the corresponding disounting factors after.

  • Object orientated programming allows us to build a structure to be able to efficiently encapsualte, record and swap out different financial models and their correpsonding parameters, by creating a pre-designed fincancial environment structure, this structure stops us needing to code each algorithm explicitly but instead allows us the flexibility to just change the chosen parameters for our desired derivative.

  • The financial environment structure encompasses an array of classes (which I have grouped as a derivatives package) that are pre-produced containing useful features and functions that are required across the simulation of all derivatives instruments. Identifiying this common functionality allows us to leverage it to write dynamic code that is highly efficient in production usage.

  • In this project I will create a full derivatives package, step by step, for modelling european derivative within a defined financial environment. The objective of the project will be to, by the end, value a call option on the same Apple stock seen in my "monte carlo simulation of stochastic processes" project to show how procedural programming can be turned into functional programming.

image.png

Step 1 - Standard Normal Random Number Generator Class

Monte-carlo simulation has random number generation at its core therfore it makes sense to have a dynamic class that can cope with all the random number generation needs of any stochastic processes regardless of its complexity e.g. Just a simple goemetric brownian motion with one stochastic varialable or hestons stochastic volatily with two.

Therefore I will start by constructing a standard normal random number generator class, a simple example of how to utilise the class is showcased after the class code.

In [59]:
# Adam's derivatives package
#Standard_normal number generator is required for some of the most well known stochastic processes, geometric brownian 
# motion and Hestons stochastic volatilty to name a few so it would be efficient to have a ready made class to produce these.
#sn_number_generator.py

import numpy as np

def sn_rand_numbers(shape, antithetic=True, moment_matching=True, fixed_seed=False):
    """ Returns an numpy array object of shape, shape, standard normally distrbuted random numbers in shape x,y,z. 
    The shape we input as x ; number of stochastic proccess in projection formula, y, number of time deltas to project,
    z number of simulations to be run 
        
    Args:
    shape: tuple (x,y,z)
        generation of array of shape (x,y,z)
    fixed_seed: Boolean
        flag to fix the seed, fixing the seed allows for repeatability of results
        
    Returns:
    
    sn_ran_nums: (x,y,z) array of sn random numbers
    """
    
    if fixed_seed:
        #pick arbitrary random seed, in this case 1000 that is repeated if fixed seed remains True
        np.random.seed(1000)
        
    if antithetic:
        #The antithetic variates technique reduces the variance of the simulation results.
        ran_array = np.random.standard_normal((shape[0], shape[1], shape[2]//2))
        ran_array = np.concatenate((ran_array,-ran_array),axis=2)
    else: 
        ran_array = np.random.standard_normal(shape)
    
    if moment_matching:
        #moment matching standradizes the random numbers
        ran_array = ran_array - np.mean(ran_array)
        ran_array = ran_array /np.std(ran_array)
    
    if shape[0] ==1:
        return ran_array[0]
    else:
        return ran_array
    
        
if __name__ == '__main__':   

    """ simple example of how to use the class, we will generate the random numbers needed for a geometric brownain
    motion sumlation with 5 time deltas and 4 simulations
    """
    random_numbers = sn_rand_numbers((1,5,4))
    
    
    print("\n" + "Below is an example of the sn random numbers required for 4 simulation paths with 5 corresponding time deltas"+"\n")
    print(random_numbers)
Below is an example of the sn random numbers required for 4 simulation paths with 5 corresponding time deltas

[[ 1.8857206   0.50184742 -1.8857206  -0.50184742]
 [-0.78350597 -0.38528375  0.78350597  0.38528375]
 [-0.65268731  1.07618784  0.65268731 -1.07618784]
 [ 0.83022077  1.24730814 -0.83022077 -1.24730814]
 [ 1.25594585  0.15250156 -1.25594585 -0.15250156]]

Step 2 - Creating a Discounting Class

To calculate the present value of a european derivative we need to be able to work out the present value of all future payoff cashflows. We need a class that is able to take any Maturity date for any derivative and any input start date and be able to return to us the corresponding discount factor.

As the sum of (the simulation payoff cashflows * discount factor) / number of simulations gives us the expected present value of the european derivative.

I am using a constant short rate model in this project therefore the title of the class is CSR_Discounter. A simple example of how to use the specific class shown blow after the class definition

In [62]:
# Adam's derivatives package
# The CSR_Discounter class constructs an array of discount factors (a discount curve) associated with a series of future dates
# formulated using a constant short rate so that financial instruments value projections from our monte carlo simulations can
# be discounted from different future dates to the current specified date.
# constant_short_rate.py

import numpy as np
import pandas as pd
import datetime as dt
import sys
sys.path.append(r'C:\Users\Adam\Desktop\Personal\Python Syntax\Derivatives_environment')

class CSR_Discounter:
    """class for constant short rate discounting     
    Args:
    name: string 
        name of the short rate being used (GBP short rate ..etc)
    short_rate: float
        constant rate for discounting 
        
    Methods
    get_discount_factor:
        returns an array of discount factors from array of datetime object inputs, the first element being used as the start date and the rest end dates
    """
    
    def __init__(self,name, short_rate):
        self.name = name
        self.short_rate = short_rate
        
    def create_discount_curve(self, dates_list, datetimeobjects=True, day_count=365):
        """ Calculates the discount factors associated with a list of time deltas established from the input passed to the method.
        The time deltas are calulated as the time differnece between each element and the first element, presumed to be the start date"""
         
        if datetimeobjects: 
            start = dates_list[0]
            delta_list = [(date - start).days/day_count for date in dates_list]
                        
        else:
            delta_list = dates_list
        
        df_list = np.exp(- self.short_rate * np.sort(delta_list))

        discount_curve = np.array((dates_list,df_list)).T
        
        #returns time delta with corresponding discount factor
        return discount_curve
    
    
        

if __name__ == '__main__': 
    """ example of how the class works - we can input 3 dates, the first always being the start date and it will return the discount
    factors associated with the other dates"""
    
    # initialising an array of date time objects  01/05/2020 , 01/10/2020 and 01/05/2021 
    dates = [dt.datetime(2020,5,1), dt.datetime(2020,10,1),dt.datetime(2021,5,1)]

    #initiate the constant short rate (CSR)
    csr = CSR_Discounter('csr',0.02)

    #Calculate the discount factors associated with those dates utilising the CSR
    discount_curve = csr.create_discount_curve(dates)
    
    #as you can see the first date is taken as the current date so its associated dicount factor is 1
    #if we were discounting the projected value of a stock at 01/10/2020 the discount factor would be 0.992
    
    print('*' *125)
    print("\n" +'The current date is: {}'.format(discount_curve[0][0]))
    print("\n"+'*' *125)
    print('\n' + 'For a payoff cashflow date at {0} the corresponding discount factor would be {1} '.format(discount_curve[1][0],round(discount_curve[1][1],3)))
        
    print('For a payoff cashflow date at {0} the corresponding discount factor would be {1}'.format(discount_curve[2][0],round(discount_curve[2][1],3)))
    print("\n"+'*' *125)
*****************************************************************************************************************************

The current date is: 2020-05-01 00:00:00

*****************************************************************************************************************************

For a payoff cashflow date at 2020-10-01 00:00:00 the corresponding discount factor would be 0.992 
For a payoff cashflow date at 2021-05-01 00:00:00 the corresponding discount factor would be 0.98

*****************************************************************************************************************************

Step 3 - Creating a Market Environtment Class and Inheritance

Because there are so many parameters involved with running a simulation for calculating the value of a derivative, it would be really useful to have a class that could store and call all of those paramters. Therefore we would always be able to tie back any caclucated derivatives values to the corresponding parameters that produced it for strong record keep. My market_environment class does just that.

Also we get to utilise the power of OOP for the first time as we can set the CSR_Discount Object as a parameter, (it will act as the short rate parameter function for our future calculations). Becuase the CSR_dicount variable is a class object we can call its attributes to add parameters to our financial envionment.

In [64]:
# Adam's derivatives package
# Establishes a market environment - a market environment is essential a class for storing all the paramters and market setting for the valualtion of 
# an instrument. Is is used before any valuation to inut and store all the market settings. It means for any valuation you can check the parameters that 
# relate to it by calling the get parameter/ yield curves/ lists methods or update paramters and run the valuations again. You can also add any simulation paramaters
# such as the number of simulations ot be run.
# The power of object orientated programming comes from the ability to store and to process data.


#market_environment.py

class market_environment:
    """class to model a market environment relevant for valuation     
    Args:
    name: string 
        name of the market environment 
    date: (datetimeobject)
        date in which the environment is established 
        
    Methods
    add_constant: 
        adds a constant (e.g. model paramter)
    get_constant:
        gets a constant (returns specified parameter)
    add_list: 
        adds a list
    get_list:
        gets a list
    add_curve:
        adds a market curve (such as a yield curve)
    get_curve:
        gets a market curve
    add_environment:
        adds and and overwerites whole environments with constants lists etc.. 
    """
    
    def __init__(self,name, pricing_date):
        self.name = name
        self.pricing_date = pricing_date #date that environment relates too
        self.constants = {} # intialise constants dictionary
        self.lists = {} # intialise lists dictionary
        self.curves= {} # intialise cruves dictionary
        
    def add_constant(self, key, constant):
        self.constants[key] = constant
        
    def get_constant(self, key):
        return self.constants[key] 
    
    def add_lists(self, key , list_object):
        self.lists[key]= list_object
        
    def get_lists(self, key):
        return self.lists[key] 
    
    def add_curve(self, key , curve):
        self.curves[key]= curve
        
    def get_curve(self, key):
        return self.curves[key] 
    
    def add_environment(self, env):
        #overwrite current environment values, with a different market_environment object 
        self.constants.update(env.constants)
        self.lists.update(env.lists)
        self.curves.update(env.curves)
        

if __name__ == '__main__':   

    """ simple example of how to use the class by creating a theoretical environment  """

    Market_Env = market_environment('new_env',dt.datetime(2020,5,18))
    Market_Env.add_constant('currency','GBP')
    Market_Env.add_constant('paths', 10000)

    # return number of simulations associated with environment
    print('*' *125)
    print('\n' + 'The number of simulations within this modelling environment is set to: {}'.format(Market_Env.get_constant('paths')))
    print('\n' +'*' *125)
    
    """ how to incorporate constant_short_rate into the class """
    # update the Pythonpath so we can reference the constant short rate module
    import sys
    sys.path.append(r'C:\Users\Adam\Desktop\Personal\Python Syntax\Derivatives_environment')
    
    import numpy as np
    import datetime as dt
    from constant_short_rate import CSR_Discounter

    #initiate a constant short rate object 
    csr = CSR_Discounter('csr',0.02)
    
    # next 3 months to be used to create a discount curve
    dates = [dt.datetime(2020,5,1), dt.datetime(2020,6,1),dt.datetime(2020,7,1),dt.datetime(2020,8,1)]

    #intitate discount curve
    discount_curve = csr.create_discount_curve(dates)
    
    #set the imported function as an attribute of the market environment 
    Market_Env.add_constant('csr',csr.short_rate)
    
    #set the imported function as an attribute of the market environment 
    Market_Env.add_curve('discount_curve',discount_curve)

    print('\n' +'The constant short rate in the financial environment is set to: {}'.format(Market_Env.get_constant('csr')))
    print('\n' +'*' *125)
    
    print('\n' +'The discount curve for the inputted payoffs dates is as followed:' + '\n')      
    print(Market_Env.get_curve('discount_curve'))
*****************************************************************************************************************************

The number of simulations within this modelling environment is set to: 10000

*****************************************************************************************************************************

The constant short rate in the financial environment is set to: 0.02

*****************************************************************************************************************************

The discount curve for the inputted payoffs dates is as followed:

[[datetime.datetime(2020, 5, 1, 0, 0) 1.0]
 [datetime.datetime(2020, 6, 1, 0, 0) 0.9983028117186761]
 [datetime.datetime(2020, 7, 1, 0, 0) 0.9966631140667146]
 [datetime.datetime(2020, 8, 1, 0, 0) 0.9949715891090928]]

Step 4 - Creating a Path Simulation Class

Every stochastic process coded for the projection of an instruments value has some similar requirements:

  • they all need to be able to extract the parameters established within the financial environemt to feed their respective projection algorithmns
  • they need to be able to generate a time grid for storing the projected instrument values over the environments inputted time deltas and simulations
  • and they need to be able to generate the valuation path simulations by running the inputted projection algorithmn.

Becuase we have again identified some silimarities, this time across different projection algorithmns it would again be efficient to code these features into an overaching class!

The base simulation class below acts as a master class to all indiviudal projection alogirithmns that could come after, Geomteric brownain motion, jump diffusion etc. These repeatable features will will be "inherited" by the projection algorithm subclasses whenever a subclass is intiatied as the subclass accepts this master class as an object and therefore inherits its initiation method.

In [ ]:
# Adam's derivatives package
# Simulation class - The simulation class acts as a master class for all further simulation classes I might build using different projection algorithmns. 
# This is becuase every projection algorithm shares some things it needs to be able to do such as: call its record paramters, call its most recently generated
# paths or run the generation if its hasn't been yet and to create a time grid to store the date times corresponding to the generated value. 

# This class also provides correlaiton functionality for the generation of multiple paths for different derivatives ina portfolio.
# simulation_class.py

import numpy as np 
import pandas as pd 


class simulation_class:
    """provides the base fromeworks for simulation classes, it provides the time grid capability and the ability to 
    return current instrument values
    
    Args:
    name: str
        name of the object
    mar_env: instance of market_environment 
        market_environment data for simulation 
    correlation: bool 
        True if correlated with other model object
        
    Methods
    
    generate_time_grid: 
        returns time grid for simulation
        
    get_instrument_values:
        returns the current instrument values (array): 
    """
    
    def __init__(self, name, market_env, portfolio=False):
        
        #pulls all attributes using market_env methods
        self.name = name
        self.pricing_date = market_env.pricing_date
        self.initial_value = market_env.get_constant('initial_value')
        self.volatility = market_env.get_constant('volatility')
        self.final_date = market_env.get_constant('final_date')
        self.currency = market_env.get_constant('currency')  
        self.frequency = market_env.get_constant('frequency')      
        self.paths = market_env.get_constant('paths')
        self.discount_curve = market_env.get_curve('discount_curve')
        try:
            # check to see if the market_env has a time_grid inputted
            self.time_grid = market_env.get_list('time_grid')
        except: 
            self.time_grid = None  
        try: 
            # check if there are any speical dates and add them if so
            self.special_dates =market_env.get_list('special_dates')
        except:
            self.special_dates = []
        # makes sure this is reset whena class is initiatied 
        self.instrument_values = None
        
        self.portfolio = portfolio
        if portfolio: 
            #only needed in a portfolio context when risk factors are correlated, cholesky matrix needs to have been inputted to market env
            self.portfolio_cholesky_matrix = market_env.get_list('portfolio_cholesky_matrix')
            self.rn_set = market_env.get_list('rn_set')[self.name]
            self.random_numbers = market_env.get_list('random_numbers')
            
    def generate_time_grid(self):
        """ Generate a time grid, an array of times for which we want to project the insturment values too this time grid
        will later feed our dicount_factor class so we can convert the dates to time deltas and calclate the discount factors
        from those duration to get the present values of the projected values - the intial date end date and time delta need to be 
        specified in the market env already. eg. pricing date = 01/05/2020, final date = 01/05/2021 and frequency = """
        
        start = self.pricing_date
        end = self.final_date
        time_grid = pd.date_range(start=start, end=end, freq=self.frequency).to_pydatetime()
        #converts array to list as the discount factor class accepts a list
        time_grid = list(time_grid)
        
        #enhance time_grid by start, end, and special_dates as pydatetime doesnt return exact start and end it gives end of month values
        if start not in time_grid: 
            #insert start date if it doesnt happen to be in the list same with end
            time_grid.insert(0,start)
        if end not in time_grid:
            time_grid.append(end)
                
        #if there are any specific future dates they are added here and must have been recorded in the market env
        if len(self.special_dates)>0:
            #add all special dates
            time_grid.extend(self.special_dates)
            #drop duplicates - set returns only unique elements
            time_grid = list(set(time_grid))  
            #sort_list as new dates have been appended 
            time_grid.sort()
        self.time_grid = np.array(time_grid)
         
    def get_instrument_values(self, fixed_seed=True):
        if self.instrument_values is None: 
            # runs the generate_paths sub class if it hasnt been run yet 
            self.generate_paths(fixed_seed=fixed_seed, day_count=365)
        elif fixed_seed is False: 
               #also initiate resimulation when fixed_seed is False
               self.generate_paths(fixed_seed = fixed_seed, day_count =365)
        return self.instrument_values 
                 

Step 5 - Creating a Path Projection Algorithmn - Geometric Browniain Motion

Now that we have created a master class for instrument projection we can create a subclass off of it. In this case I will create Geometric Brownain motion class with code very similar to that coded in the procedural paradigm as opposed to an object orientated paradigm.

After the geometric_brownian_motion defintion we will utilise the power of OOP to project the current Appl Stock value, in a financial environment created to micmic the current financial conditions in the US. This will be done pulling together all the classes generated thus far showing the power of OOP and to showcase its easy to follow script.

In [65]:
# Adam's derivatives package
# Simulation sub class - Geometric_Brownian_motion, the Geometric_Brownian_motion class is a subclass to the simulation class, this is a class that carries
# out the actual financial instrument value projection, in this case using the GMB Sotchastic process algorithmn. 
# square_root_diffusion.py


import sys
sys.path.append(r'C:\Users\Adam\Desktop\Personal\Python Syntax\Derivatives_environment')
    
import numpy as np
import datetime as dt
from sn_rand_generator import sn_rand_numbers
from constant_short_rate import CSR_Discounter
from market_environment import market_environment 
from simulation_class import simulation_class
 

class geometric_brownian_motion(simulation_class):
    """ Class to generate simulated paths based on the square root diffusion model
    
    Args:
    name: string
        name of the object
    mar_env: market environment object
        market environemt
    corr: Boolean 
        True if correlated with other model object
        
    Methods: 
    update:
        update the parameters in the stochastic process
    generate_paths :
        returns Monte Carlo paths given the market environment 
    """
    
    #def __init__ (self, name, market_env, portfolio=False):
    #    super(geometric_brownian_motion, self).__init__(name, market_env, portfolio)
      
    def update(self, initial_value=None, volatility=None, final_date =None):
        """ this is where the GMB projection parameters are added in, the intial value would be the stock price at
        the pricing date (current time) if we wanted to value an equity derivative"""
        
               
        if initial_value is not None:
            self.initial_value = initial_value
        if volatility is not None:
            self.volatility = volatility
        if final_date is not None:
            self.final_date = final_date
        #set the instrument values back to zero as the parameters have been updated and paths havent been generated yet
        self.instrument_values = None
        
    def generate_paths( self, fixed_seed=True, day_count=365.):
        """ class to carry out the projection of the financial instrument value """
        
        #firstly generate the time grid using the inherited generate time grid method
        if self.time_grid is None:
            self.generate_time_grid()
                
        # the number of date inteverals 
        M = len(self.time_grid)
        # the number of simulations 
        I = self.paths
        
        #intiaite the two arrays for the volatility projection process, one for the volatility parameter and one for the non negative volatility value
        paths = np.zeros((M,I))
        paths[0] = self.initial_value
                
        # check to see if this instrument is correlated with others we are simulating if its not we create the random numbers here
        # if it is then we use the random numbers stored within the market environment and pre prepared
        if not self.portfolio: 
            ran_array = sn_rand_numbers((1,M,I), fixed_seed=fixed_seed)
            
        else:
            # get random numbers defined in market environment, used for portoflio analysis
            ran_array = self.random_numbers
            
        #establish the paramters of the euler discretization of GMB
        short_rate = self.discount_curve.short_rate
            
        for t in range(1, M):
            #select thr right time slice from the random number set 
            if not self.portfolio:
                ran_num = ran_array[t]
            else:
                ran_num = np.dot(self.cholesky_matrix, ran_array[:,t,:])
                ran_num = ran_num[self.rn_set]
            
            #the dt time deltas are calulcated incrementally as the positions in time between the times in the time grid
            #this is becuase special dates need functinoal incorporation
            dt = (self.time_grid[t] - self.time_grid[t-1]).days / day_count
            
              
            # full GMB discretized equation 
            paths[t]= paths[t-1] * np.exp((short_rate - 0.5 * self.volatility **2) * dt + self.volatility * np.sqrt(dt) * ran_num) 
        
        # generate projected values
        self.instrument_values= paths
    

if __name__ == '__main__': 
    """ example of how the class works - simulating the projection of Apple stock utilising geometric brownian motion
    set within a financial and simulation environment establishing utislising the classes above"""
    
    import sys
    sys.path.append(r'C:\Users\Adam\Desktop\Personal\Python Syntax\Derivatives_environment')
    import numpy as np
    import datetime as dt
    from constant_short_rate import CSR_Discounter
    from market_environment import market_environment 
    from simulation_class import simulation_class
    from sn_rand_generator import sn_rand_numbers
    
    
    """step 1 initaite market environment and add all parameters """   
    APPL_Market_env = market_environment('APPL_Market_env', dt.datetime(2020,5,1))
    
    APPL_Market_env.add_constant('initial_value', 311)
    APPL_Market_env.add_constant('currency', 'USD')
    APPL_Market_env.add_constant('volatility', 0.228)
    APPL_Market_env.add_constant('final_date', dt.datetime(2024,5,1))
    APPL_Market_env.add_constant('frequency', 'M')
    APPL_Market_env.add_constant('paths', 10000)
    
    #intitate Discounting Variable
    csr = CSR_Discounter('csr', 0.0016)
    APPL_Market_env.add_curve('discount_curve', csr)
    
    """ step 2 intiate Simulation generator that accepts market environment """
    Appl_gbm = geometric_brownian_motion('gbm', APPL_Market_env)
    
    """ step 3 establish a time grid """
    Appl_gbm.generate_time_grid()
    
    #print the time grid 
    Appl_gbm_timegrid = Appl_gbm.time_grid
    
    """ step 4 generate paths """ 
    Appl_gbm.generate_paths()
    #print paths
    Apple_paths = Appl_gbm.instrument_values.round(2)
    
    """ plot the dynamically simulated geometric Brownaian motion paths"""
    import matplotlib.pyplot as plt
    
    fig1, ax = plt.subplots(figsize=(16,8))
    ax.plot(Apple_paths[:,:10], lw=1.5)  # only plot 10 simulation paths and set line width to 1.5
    ax.set_title('10 Simulated Apple Stock Projections Over the Next 5 Years')
    ax.set_xlabel('Time in months')
    ax.set_ylabel('Apple Stock Price ($)')

Step 6 - Creating a Valuation Class

Just as with the simulation class, where we identified similarities between all projection algorithmn, there are also similarities between all derivatives valuation procedures, but particularly in our case, the valuation of european options:

  • All european options have a payoff function derived from a relationship bewtween a predefined "strike price" and the value of an underlying instrument
  • and they again require inputs from their financial environment, in particular, the discount curve to enact the discounting of the maturity payoffs to calculate the present value of the derivative.

Having these common similarities once again means porduction efficiencies can be achieved by coding this functinality into an over arching master class function so it can be leveraged repeatedly.

Below I have therefore coded a masterclass that could be inherited by future european option classes, in our case we will be implmenting a european call option class further down.

In [ ]:
# Adam's derivatives package
#   Valuation Class - base class, the valuation class sets the variables for tje simulaiton of all valuation classes
# whether thye be europeean calls, american puts etc. These derivaties share features that are better built out in base 
# class so that all can future valuation classes can use the reporducable functionality of the base class. 
# This class also provides methods for calulcates the vega and theta (greeks) of any valuation classes that are initiated. 
# square_root_diffusion.py


class valuation_baseclass:
    """ this class will instiate all the main varibales required for the reunning of a valuation algorithmn
    
    args: 
    name: str
        name of the valuation base class: 'valuation_bc'
        
    projection_algorithm: instance of a simulation class
        this is the algorithmn for projecting the underlying instruments value
        
    market_env: instance of market environment object
        this is the established market environment in which you want to run the derivative valuation algorithm
        
    payoff_func: str
        deivatives payoff in Python syntax
        Example: 'np.maximum(maturity_value - strike price,0)'
        where maturity value is the numpy vector with respective values of the underlying 
        
    
    Methods: 
        
    update:
        updates the paramters of the valuation instance
    delta:
        returns the delta of the derivative
    vega:
        returns the vega of the derivative
    """

    def __init__(self, name, proj_algo, market_env, payoff_func=''): 
        """
        setting up all the attributes neccessary to run a valuation algorithm inc: market environment parameter and
        underyling instrument projection parameters"""
        
        # attributes from market environment object
        self.name = name 
        self.pricing_date = market_env.pricing_date
        self.maturity = market_env.get_constant('maturity')
        self.currency = market_env.get_constant('currency')
        
        # attributes from projection algo object
        self.frequency = proj_algo.frequency
        self.paths = proj_algo.paths
        self.discount_curve = proj_algo.discount_curve
        self.payoff_func = payoff_func
        self.proj_algo = proj_algo 
        
        # see if strike price has been entered to market environment
        try:
            self.strike_price = market_env.get_constant('strike_price')
        except:
            pass
        
        #provide the pricing date and the maturity date to the projection algorithmn
        self.proj_algo.special_dates.extend([self.pricing_date,self.maturity])
    
        def update(self, intial_value = None, volatility= None, strike_price=None, maturity = None):
            """ method is used to updated any paramters to do with runnning the valuation class, market_env paramters
            or updates to the derivative maturity etc """
            
            # update the projection_algo object if any of its required parameters have changed
            if intial_value is not None: 
                self.proj_algo.update (intial_value=intial_value)
            if volatility is not None:
                self.proj_algo.update(volatility=volatility)
           
            # update the valuation object if any of its required parameters have changed.
            if strike is not None: 
                self.strike_price = strike_price
            if maturity is not None: 
                self.maturity = maturity 
                # need to add the maturity date to the proj algo time grid
                self.proj_algo.special_dats.extend(maturity)
                # if the maturity date has been updated we also need to erase the instruments current values
                self.instrument_values = None
                

Step 7 - Creating a European Call Option Class

Now that we have a built a valuation framework class we can build the described specific subclass required to take on our own derivative valuation task.

This subclass inherits all the functionality of the framework masterclass and it is also able to retreive all the required parameters from the market environment, as well as as those from the projection algorithmn (which has inherited the simulation classes functionality) as it accepts these as attributes along with the specific payoff function and maturity values which are user defined on intiation.

Pulling this all together, aslong as we have the required parameters we will be able to estimate the expected present value of any european call option!

In [36]:
class valuation_eur_option(valuation_baseclass):
    """ class for generating the vlaue of a european option with  arbritray payoff it inherets all attributes
    of the valuation base class 
    
    Methods: 
    generate_payoffs:
        returns maturity payoffs from the derivative given siumulated projections of the underlying instrument
        
    generate_present_value:
        returns the expected value of the derivative (monte carlo estimator)
    
    """    
    def generate_payoffs(self,fixed_seed=False):
        
        
        strike_price = self.strike_price
        
        paths = self.proj_algo.get_instrument_values(fixed_seed=fixed_seed)
        time_grid = self.proj_algo.time_grid      
        
        #find the index of the maturity date may not be the end as its entered as a special date
        time_index  = np.where(time_grid==self.maturity)[0]
        time_index = int(time_index)
        """ maturity values variables is the same varibale name that needs to be used in the defined payoff function """
        maturity_values = paths[time_index]
        
        try:
            #eval works by reading a string as if it is code, so the payoff _func needs to read:
            #    np.maximum(maturity_values - strike_price,0)   for a call option and
            #    np.maximum(strike_price - maturity_values,0)   for a put option    
                
            maturity_payoffs = eval(self.payoff_func)
        except:
            print('Error evaluating payoff function.')
            
        self.maturity_payoffs = maturity_payoffs
        
        return maturity_payoffs
   
    def generate_present_value(self, accuracy=6, fixed_seed=False, full=False):
        """
        args:
        accuracy: int
        Number of decimals the result will be rounded too
         
        fixed_seed: Bool 
            Whether the random seed is fixed
        
        full: Bool
            Whether a full array of all sims is returned 
        """    
         
        if len(self.maturity_payoffs) > 0:
         
            maturity_payoffs = self.generate_payoffs(fixed_seed=fixed_seed)
            discount_factor = self.discount_curve.create_discount_curve([self.pricing_date,self.maturity])[0,1]
         
            # summing up all the payoff cashflows then divided by the number of payoff cashflows is essentiall multiply each cashflow by its
            # probability of happeining therefore returning the expected payoff and discounting that gives us the expected present value! 
            self.expected_pvalue = discount_factor * np.sum(maturity_payoffs )/ len(maturity_payoffs)
         
        else:
              
            discount_factor = self.discount_curve.create_discount_curve([self.pricing_date,self.maturity])[0,1]
            self.expected_pvalue = discount_factor * np.sum(maturity_payoffs )/ len(maturity_payoffs)
         
        return self.expected_pvalue
         

STEP 8 - Pulling it All Together - The Power of Object Orientated Programming!

Now I will display how all of the above is pulled together in practice to carry out the valuation of a european call option on Apple stock with the following paramters all taken from my prior project on stochastic processes:

  • current price of apple stock: 311 dollars
  • Apple volatility of: 0.228
  • strike price of the call option: 300 dollars
  • time to maturity: 4 years
  • short rate: 0.0016
  • Frequency of time deltas: 1 month
  • simulations: 10000

The result comes out at 24 dollars!

On my own PC each class is saved within its own python module which is called at the start of the code (as shown below) these aren't particularly relevant for this jupyter notebook as each class has been complied as we went along but it provides context as to how all the above code could be stored seperately within a production environment for easy reviewal, enhancement or audit keeping.

This is one of the powerful features of python and allows for faster development compared to other classes.

In [66]:
if __name__ == '__main__': 
    """ example of how the full derivatives package works  """
    
    """ these imports are only relevant for running on my personal computer I import classes which are saved in seperate module
    the sys.path.append updates my pythonPath so to where all the modules are stored"""
    
    import sys
    sys.path.append(r'C:\Users\Adam\Desktop\Personal\Python Syntax\Derivatives_environment')
    import numpy as np
    import datetime as dt
    from sn_rand_generator import sn_rand_numbers
    from constant_short_rate import CSR_Discounter
    from market_environment import market_environment
    from simulation_class import simulation_class
    from geometric_brownian_motion import geometric_brownian_motion
    from valuation_baseclass import valuation_baseclass
    
    """step 1 initaite market environment and add all parameters """   
    
    APPL_Market_env = market_environment('APPL_Market_env', dt.datetime(2020,5,1))
    APPL_Market_env.add_constant('initial_value', 311)
    APPL_Market_env.add_constant('currency', 'USD')
    APPL_Market_env.add_constant('volatility', 0.228)
    APPL_Market_env.add_constant('final_date', dt.datetime(2024,5,1))
    APPL_Market_env.add_constant('frequency', 'M')
    APPL_Market_env.add_constant('paths', 10000)
    #intitate Discounting Variable
    csr = CSR_Discounter('csr', 0.0016)
    APPL_Market_env.add_curve('discount_curve', csr)
    
    """ step 2 intiate Simulation generator that accepts market environment """
    Appl_gbm = geometric_brownian_motion('gbm', APPL_Market_env)
    
    """ step 3 - in addition to a projection objection we also need to define a market environemtn for the option itself.
    it has to conatin at leat a maturity and a currency. Optionally a value for the strike paramter can be included aswell
    dependent upon how you write the payoff function input """
    
    Appl_gbm_call_env = market_environment('Appl_gbm_call', Appl_gbm.pricing_date)
    Appl_gbm_call_env.add_constant('maturity', dt.datetime(2021,5,1))     
    Appl_gbm_call_env.add_constant('strike_price', 300)  
    Appl_gbm_call_env.add_constant('currency', 'USD') 
     
    """ STEP 4 - we need to define the payoff function we are waning to model a call so we need the below function """
    
    payoff_func = 'np.maximum(maturity_values - strike_price,0)'
     
    """Step 5 - bring it all together and intiate the option valuation class """
    
    Appl_eur_call = valuation_eur_option('appl_eur_call', proj_algo=Appl_gbm, market_env = Appl_gbm_call_env , payoff_func = payoff_func )
   
    """ Step 6 - generate the payoffs """
    
    Appl_eur_call_payoffs = Appl_eur_call.generate_payoffs()
     
    """ Step 7 - generate the present value of the option """ 
    
    Appl_eur_call_pv = Appl_eur_call.generate_present_value()
                      
    fig1, ax = plt.subplots(figsize=(16,8))
    ax.hist(Appl_eur_call_payoffs,bins=50)
    ax.set_title('Histogram of payoffs of a Euopean Call option on Apple Stock at a strike price of $300 and 4 year Maturity')
    ax.set_xlabel('Projected Appl_eur_call_payoffs at maturity')
    ax.set_ylabel('Frequency')
    ax.plot()
    
    print( '*' *125)
    print('\n'+ 'The expected present value of the European call option on Apple stock with a strike price of $300 is: ${}'.format(round(Appl_eur_call_pv,2)))
    print( '\n' + '*' *125) 
        
    print('\n'+ 'Below you will find the simulated pay off values of the derivative they are distributed in a standard normal distribution shape \
          due to the standard normal distribution element with geometric brownaian motion')      
       
*****************************************************************************************************************************

The expected present value of the European call option on Apple stock with a strike price of $300 is: $24.0

*****************************************************************************************************************************

Below you will find the simulated pay off values of the derivative they are distributed in a standard normal distribution shape           due to the standard normal distribution element with geometric brownaian motion