├── Bellman-Ford.png ├── README.md ├── agent-environment.png ├── dl-agent.py ├── env_market.py ├── performce-episodes.png ├── plot_reward.py ├── pyprocess.py ├── reward.txt ├── reward_plot.png └── simulated-pair-prices.png /Bellman-Ford.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaurav1086/dl-algo-trader/bd8b54717694594a42adc6876bec0e518bfe5aff/Bellman-Ford.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Deep Reinforcement Learning for Algorithmic Trading

2 | 3 | Training an Agent to make automated long/short trading decisions in a simulated stochastic market environment using 4 | Deep Q-Reinforcement Learning. 5 | 6 | #Dependencies 7 | Python version 3.7.5 8 | 9 | Packages: 10 | pip install tensorflow keras pandas matplotlib 11 | 12 | Steps to run: 13 | 1. python dl-agent.py #One run of 500 episodes 14 | 2. python plot_reward.py #Plots the reward time series 15 | 16 |

IMPLEMENTATION

17 | 18 | Based on the investment thesis of the mean reversion of the spreads, I will simulate 500 episodes of two mean reverting stochastic processes and train the agent to do a long/short strategy. Think of it as two instruments (stocks or bonds) belonging to the same industry sector which more or less move together and the agent i.e. Neural Net is the trader who will exploit the aberrations in their behavior due to news, earnings report, weather or other macro-economic events by going long on the cheaper instrument and short on the expensive one and vice versa until it reverts back to its mean. In fact, the Neural Net wouldn’t even know about the mean reversion behavior or whether to do a statistical arbitrage strategy or not, instead it will discover this pattern by itself in its pursuit to maximize the rewards/gains in every episode, i.e. it will learn this strategy by itself through trial and error. Once trained in this environment, this agent should be able to trade any two instruments which have a certain co-integrationbehavior and respective volatility range. We can safely assume that the trading volume is small enough so as to have no impact whatsoever on the market. I would like to re-emphasize the importance of generating unbiased data as opposed to using historical market data as I have defined the concept as ‘Smart Data’ in my previous post. 19 | 20 |

ENVIRONMENT

21 | 22 | The first and the most important part is to design the environment. The environment class should implement the following attributes / methods based on the OpenAI / gymconvention: 23 | 24 | Init : For initialization of the environment at the beginning of the episode. 25 | 26 | State: Holds the price of A and B at any given time = t. 27 | 28 | Step: The change in environment after one time step. With each call to this method, the environment returns 4 values described below: 29 | 30 | a) next_state: The state as a result of the action performed by the agent. In our case, it will always be the Price of A and B at t = t + 1 31 | 32 | b) reward: Gives the reward associated with the action performed by the Agent. 33 | 34 | c) done: whether we have reached the end of the episode. 35 | 36 | d) info: Contains diagnostic information. 37 | 38 | Reset: To reset the environment after every episode of training. In this case, it restores the prices of both A and B to their respective means and simulates new price path. 39 | 40 | Its a good practice to keep the environment code separate from that of the agent. Doing so, will make it easier to modify the environment’s behavior and training the agent on the fly. I wrote a Python class called market_env to implement its behavior. 41 | 42 | A sample path of 500 time steps for the two assets generated by the environment with A(blue): mean = 100.0, vol = 10% and B(green): mean = 100.0, vol = 20% using the Ornstein–Uhlenbeck process (plotted using python/matplotlib) is shown below. As you can see that the two processes cross each other many times exhibiting a co-integration property, an ideal ground to train the agent for a long-short strategy. 43 | 44 | ![Simulated Pair Prices](https://github.com/gaurav1086/dl-algo-trader/blob/master/simulated-pair-prices.png) 45 | 46 |

AGENT

47 | 48 | The agent is a MLP (Multi Layer Perceptron) multi-class classifier neural network taking in two inputs from the environment: Price of A and B resulting in actions : (0) Long A, Short B (1) Short A, Long B (2) Do nothing, subject to maximizing the overall reward in every step. After every action, it receives the next observation (state) and the reward associated with its previous action. Since the environment is stochastic in nature, the agent operates through a MDP (Markov Decision Process) i.e. the next action is entirely based on the current state and not on the history of prices/states/actions and it discounts the future reward(s) with a certain measure (gamma). The score is calculated with every step and saved in the Agent’s memory along with the action, current state and the next state. The cumulative reward per episode is the sum of all the individual scores in the lifetime of an episode and will eventually judge the performance of the agent over its training. The complete workflow diagram is shown below: 49 | 50 | ![Agent Environment](https://github.com/gaurav1086/dl-algo-trader/blob/master/agent-environment.png) 51 | 52 | Why should this approach even work ? Since the spread of the two co-integrated processes exhibits a stationary property i.e. 53 | it has a constant mean and variance over time and can be thought of as having a normal distribution. The agent can identify this statistical behavior by buying and selling A and B simultaneously based on their price spread (= Price_A — Price_B) . For example, if the spread is negative it implies that A is cheap and B is expensive, the agent will figure the action would be to go long A and short B to attain the higher reward. The agent will try to approximate this through the Q(s, a) function where ‘s’ is the state and ‘a’ is the optimal action associated with that state to maximize its returns over the lifetime of the episode. The policy for next action will be determined using Bellman Ford Algorithm as described by the equation below: 54 | 55 | ![Bellman-Ford](https://github.com/gaurav1086/dl-algo-trader/blob/master/Bellman-Ford.png) 56 | 57 | Through this mechanism, it will also appreciate the long term prospects than just immediate rewards by assigning different Q values to each action. This is the crux of Reinforcement Learning. Since the input space can be massively large, we will use a Deep Neural Network to approximate the Q(s, a) function through backward propagation. Over multiple iterations, the Q(s, a) function will converge to find the optimal action in every possible state it has explored. 58 | 59 | Speaking of the internal details, it has two major components: 60 | 61 | Memory: Its a list of events. The Agent will store the information through iterations of exploration and exploitation. It contains a list of the format: (state, action, reward, next_state, message) 62 | Brain: This is the Fully Connected, Feed-Forward Neural Net which will train from the memory i.e. past experiences. Given the current state as input, it will predict the next optimal action. 63 | 64 | To train the agent, we need to build our Neural Network which will learn to classify actions based on the inputs it receives. (A simplified Image below. Of course the real neural net will be more complicated than this.). 65 | 66 | In the above image, 67 | 68 | Inputs(2): Price of A and B in green. 69 | 70 | Hidden(2 layers): Denoted by ‘H’ nodes in blue. 71 | 72 | Output(3): classes of actions in red. 73 | 74 | For implementation, I am using Keras and Tensorflow both of which are free and open source python libraries. 75 | 76 | The neural net is trained with an arbitrarily chosen sample size from its memory at the end of every episode in real-time hence after every episode the network collects more data and trains further from it. As a result of that, the Q(s, a) function would converge with more iterations and we will see the agent’s performance increasing over time until it reaches a saturation point. The returns/rewards are scaled in the image below. 77 | 78 | ![Performance Episodes](https://github.com/gaurav1086/dl-algo-trader/blob/master/performce-episodes.png) 79 | 80 | In the above graph, you can see 3 different plots representing entire training scenarios of 500 episodes, each having 500 steps. With every step, the agent performs an action and gets its reward. As you can see, in the beginning since the agent has no preconception of the consequences of its actions, it takes randomized actions to observe the rewards associated with it. Hence the cumulative reward per episode fluctuates a lot in the beginning from 0–300th episode, however beyond 300 episodes, the agent starts learning from its training and and by 400th episode, it almost converges in each of the training scenarios as it discovers the long-short pattern and starts to fully exploit it. 81 | 82 | There are still many challenges to it and it is still a part of an ongoing research of engineering both the agent as well as the environment. My aim here was not to show a ‘backtested profitable trading strategy’ but to describe how to apply advanced Machine Learning concepts such as Deep Q-Learning/Neural Networks to the field of Algorithmic Trading. It is an extremely complicated process and pretty hard to explain in a single blog post however I have tried my best to simplify things. Check out dl-algo-trader link for the code. 83 | 84 | Furthermore, this approach can be extended into a large portfolio of stocks and bonds and the agent can be trained under diverse range of stochastic environments. Additionally, the agent’s behavior can be constrained to various risk parameters such as sizing, hedging etc. One can also have multiple agents training under different suitability criteria given the desired risk/return profiles. These types of approximation can be made more accurately using large data sets and distributed computing power. 85 | 86 | Eventually, the question is, can AI do everything ? Probably no. Can we effectively train it to do anything ? Possibly yes , i.e. with real intelligence, the artificial intelligence can surely thrive. Thanks for reading. Please feel free to share your ideas in the comment section below or connect with me on linkedin . 87 | 88 | Hope you enjoyed the post ! 89 | 90 |

DISCLAIMER

91 | 92 | 93 | 1. Opinions expressed are solely my own and do not express the views or opinions of any of my employers. 94 | 95 | 2. The information from the Site is based on financial models, and trading signals are generated mathematically. All of the calculations, signals, timing systems, and forecasts are the result of back testing, and are therefore merely hypothetical. Trading signals or forecasts used to produce our results were derived from equations which were developed through hypothetical reasoning based on a variety of factors. Theoretical buy and sell methods were tested against the past to prove the profitability of those methods in the past. Performance generated through back testing has many and possibly serious limitations. We do not claim that the historical performance, signals or forecasts will be indicative of future results. There will be substantial and possibly extreme differences between historical performance and future performance. Past performance is no guarantee of future performance. There is no guarantee that out-of-sample performance will match that of prior in-sample performance. 96 | ) 97 | 98 | 99 | Blog Link: https://medium.com/@gaurav1086/machine-learning-for-algorithmic-trading-f79201c8bac6 100 | -------------------------------------------------------------------------------- /agent-environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaurav1086/dl-algo-trader/bd8b54717694594a42adc6876bec0e518bfe5aff/agent-environment.png -------------------------------------------------------------------------------- /dl-agent.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from keras.models import Sequential 3 | from keras.layers import Dense 4 | from keras.optimizers import Adam 5 | from keras.optimizers import sgd 6 | from collections import deque 7 | import random 8 | 9 | from env_market import * 10 | 11 | steps = 500 12 | 13 | class Agent: 14 | 15 | def __init__(self): 16 | self.memory = deque(maxlen=500000) 17 | self.learning_rate = 0.001 18 | self.gamma = 0.9 19 | self.exploration_rate = 1.0 20 | self.exploration_min = 0.01 21 | self.exploration_decay = 0.95 22 | self.brain = self.build_model() 23 | 24 | self.total_reward = 0.0 #Reward at the end of every episode 25 | 26 | self.apos = 0 27 | self.bpos = 0 28 | 29 | def build_model(self): 30 | 31 | #input: price of A and B 32 | #output: 0|1 for A and 0|1 for B where 0 => buy and 1 => sell 33 | 34 | model = Sequential() 35 | model.add(Dense(20, input_dim = 1, activation = 'relu')) 36 | model.add(Dense(20, activation='relu')) 37 | model.add(Dense(20, activation='relu')) 38 | model.add(Dense(3, activation='softmax')) 39 | model.compile(optimizer='rmsprop', 40 | loss='categorical_crossentropy', 41 | metrics=['accuracy']) 42 | 43 | print("Model Created") 44 | 45 | return model 46 | 47 | def remember(self, state, action, reward, next_state, done): 48 | self.memory.append((state, action, reward, next_state, done)) 49 | 50 | 51 | def replay(self, batch_size): 52 | if (len(self.memory) < batch_size): 53 | return 54 | 55 | sample_batch = random.sample(self.memory, batch_size) 56 | 57 | #Online training with this sample 58 | for state, action, reward, next_state, done in sample_batch: 59 | if done: #End of episode 60 | target = reward 61 | else: 62 | target_f = self.brain.predict(np.array([[state.A - state.B]])) 63 | target = reward + self.gamma * np.amax(self.brain.predict(np.array([next_state.A - next_state.B]))[0]) 64 | 65 | y = np.zeros((1, 3)) 66 | 67 | y[:] = target_f[0][:] 68 | y[0][action] = target 69 | 70 | y_train = [] 71 | y_train.append(y.reshape(3,)) 72 | y_train = np.array(y_train) 73 | 74 | self.brain.fit(np.array([[state.A - state.B]]), y_train, epochs=1, verbose=0) 75 | 76 | 77 | def act(self, state): 78 | 79 | return np.argmax(self.brain.predict(np.array([[state.A - state.B]]))[0]) #0 or 1 80 | 81 | def calc_reward(self, cur_state, next_state, action): 82 | 83 | reward = 0.0 84 | 85 | if (action == 0): #Buy A Sell B 86 | reward = cur_state.B - next_state.B + next_state.A - cur_state.A 87 | self.apos = self.apos + 1 88 | self.bpos = self.bpos - 1 89 | elif (action == 1): #Sell A Buy B 90 | reward = cur_state.A - next_state.A + next_state.B - cur_state.B 91 | self.apos = self.apos - 1 92 | self.bpos = self.bpos + 1 93 | else: #Do nothing 94 | reward = 0 95 | 96 | return reward 97 | 98 | def run(self, env): 99 | 100 | cur_st = state() 101 | nxt_st = state() 102 | 103 | fp = open("reward.txt", "w") 104 | fp.write("time reward\n") 105 | 106 | 107 | for num_episodes in range(500): 108 | 109 | self.total_reward = 0.0 110 | env.reset() 111 | self.replay(100) 112 | 113 | self.apos = 0 114 | self.bpos = 0 115 | 116 | #Gather the first observation 117 | cur_st, done, msg = env.step() 118 | 119 | for num_steps in range(steps): 120 | 121 | act = self.act(cur_st) 122 | 123 | tmp_st = state() 124 | tmp_st.A = cur_st.A 125 | tmp_st.B = cur_st.B 126 | 127 | nxt_st, done, msg = env.step() 128 | 129 | #Calculate reward 130 | act_reward = self.calc_reward(tmp_st, nxt_st, act) 131 | 132 | self.total_reward = self.total_reward + act_reward 133 | 134 | self.remember(tmp_st, act, act_reward, nxt_st, done) 135 | 136 | cur_st.A = nxt_st.A 137 | cur_st.B = nxt_st.B 138 | 139 | #Episode over, liquidate everything 140 | if num_steps == steps - 1: 141 | self.total_reward = self.total_reward + self.apos * cur_st.A + self.bpos * cur_st.B 142 | break 143 | 144 | print("Episode: "+str(num_episodes)+" Total Reward: "+str(self.total_reward/1000.0)) 145 | s = str(num_episodes)+" "+str(self.total_reward/1000.0)+"\n" 146 | fp.write(s) 147 | 148 | 149 | if __name__ == "__main__": 150 | np.random.seed(1) 151 | trade_agent = Agent() 152 | env = mkt_env() 153 | Agent.run(trade_agent, env) 154 | 155 | -------------------------------------------------------------------------------- /env_market.py: -------------------------------------------------------------------------------- 1 | import pyprocess as pp 2 | import numpy as np 3 | 4 | class state: 5 | A = 100.0 6 | B = 100.0 7 | 8 | class mkt_env: 9 | s = state() 10 | index = 0 11 | timestep = 5001 12 | 13 | msg = "None" #Info/Error 14 | 15 | def __init__(self): 16 | 17 | np.random.seed(10) 18 | self.s.A = 100.0 19 | self.s.B = 100.0 20 | self.theta = 3.0 21 | self.mean = 100.0 22 | self.vol_A = 10.0 23 | self.vol_B = 20.0 24 | 25 | #Now create the processes 26 | self.p1 = pp.OU_process(self.theta, self.mean, self.vol_A, 0, self.mean, None, None) 27 | self.p2 = pp.OU_process(self.theta, self.mean, self.vol_B, 0, self.mean, None, None) 28 | 29 | self.pa = np.zeros((5000, 1)) 30 | self.pb = np.zeros((5000, 1)) 31 | 32 | def reset(self): 33 | #Generate step from processes 34 | self.index = 0 35 | 36 | self.pa = self.p1.sample_path(range(self.timestep), 1) 37 | self.pa = self.pa.reshape(self.timestep,) 38 | 39 | self.pb = self.p2.sample_path(range(self.timestep), 1) 40 | self.pb = self.pb.reshape(self.timestep,) 41 | 42 | 43 | def step(self): 44 | #Returns 3 values 45 | 46 | done = False 47 | 48 | self.s.A = self.pa[self.index] 49 | self.s.B = self.pb[self.index] 50 | self.msg = "None" 51 | 52 | self.index = self.index + 1 53 | 54 | if self.index > self.timestep - 1: 55 | done = True 56 | self.msg = "Episode over" 57 | 58 | return self.s, done, self.msg 59 | 60 | -------------------------------------------------------------------------------- /performce-episodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaurav1086/dl-algo-trader/bd8b54717694594a42adc6876bec0e518bfe5aff/performce-episodes.png -------------------------------------------------------------------------------- /plot_reward.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from matplotlib.figure import Figure 3 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 4 | import sys 5 | import matplotlib.pyplot as plt 6 | 7 | import pandas as pd 8 | 9 | dat = pd.read_csv("reward.txt", sep=" ") 10 | 11 | reward = dat['reward'] 12 | tim = dat['time'] 13 | 14 | plt.plot(reward, color='red', linewidth=0.5) 15 | 16 | plt.xlabel("TIME") 17 | plt.ylabel("REWARD") 18 | 19 | plt.show() 20 | 21 | -------------------------------------------------------------------------------- /pyprocess.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | """ 4 | PyProcess 5 | @author: Cameron Davidson-Pilon 6 | """ 7 | import numpy as np 8 | import scipy as sp 9 | import scipy.stats as stats 10 | from scipy.special import gammainc 11 | import warnings 12 | import matplotlib.pyplot as plt 13 | import pdb 14 | 15 | np.seterr( divide="raise") 16 | 17 | 18 | #TODO change all generate_ to sample_ 19 | 20 | 21 | 22 | class Step_process(object): 23 | """ 24 | This is the class of finite activity jump/event processes (eg poisson process, renewel process etc). 25 | Parameters: 26 | time_space_constraints: a dictionary contraining AT LEAST "startTime":float, "startPosition":float, 27 | 28 | """ 29 | 30 | def __init__(self, startTime = 0, startPosition = 0, endTime = None, endPosition = None): 31 | 32 | self.conditional=False 33 | self.startTime = startTime 34 | self.startPosition = startPosition 35 | if ( endTime != None ) and ( endPosition != None ): 36 | assert endTime > startTime, "invalid parameter: endTime > startTime" 37 | self.endTime = endTime 38 | self.endPosition = endPosition 39 | self.condition = True 40 | elif ( endTime != None ) != ( endPosition != None ): 41 | raise Exception( "invalid parameter:", "Must include both endTime AND endPosition or neither" ) 42 | 43 | 44 | def _check_time(self,t): 45 | if t= t ] 84 | <=> 85 | E[ N | T_1 + T_2 + .. T_{N-1} < t, T_1 + T_2 + .. T_N >= t ] 86 | where we can sample T only. 87 | 88 | """ 89 | warnings.warn("Attn: performing MC simulation. %d simulations."%n_simulations) 90 | v = np.zeros( n_simulations ) 91 | continue_ = True 92 | i = 1 93 | t = t - self.startTime #should be positive. 94 | size = v.shape[0] 95 | sum = 0 96 | sum_sq = 0 97 | while continue_: 98 | v += self.T.rvs( size ) 99 | #count how many are greater than t 100 | n_greater_than_t = (v>=t).sum() 101 | sum += n_greater_than_t*i 102 | 103 | v = v[v < t] 104 | i+=1 105 | size = v.shape[0] 106 | empty = size == 0 107 | 108 | 109 | if empty: 110 | continue_ = False 111 | 112 | if i > 10000: 113 | warnings.warn("The algorithm did not converge. Be sure 'T' is a non negative random \ 114 | variable, or set 't' lower.") 115 | break 116 | 117 | return sum/n_simulations 118 | 119 | def _var(self,t): 120 | self._check_time(t) 121 | return self._mean_number_of_jumps(t)*self.J.var() 122 | 123 | 124 | def plot(self, t,N=1, **kwargs): 125 | assert N >= 1, "N must be greater than 0." 126 | for n in range(N): 127 | path = [self.startPosition, self.startPosition] 128 | times = [self.startTime] 129 | _path, _times = self.sample_path(t,1) 130 | 131 | for i in range(2*_path.shape[1]): 132 | j = int( (i - i%2)/2) 133 | path.append( _path[0][j] ) 134 | times.append( _times[0][j] ) 135 | #path.append( _path[0][-1] ) 136 | times.append( t) 137 | #pdb.set_trace() 138 | plt.plot( times, path, **kwargs ) 139 | plt.xlabel( "time, $t$") 140 | plt.ylabel( "position of process") 141 | plt.show() 142 | return 143 | 144 | 145 | 146 | class Renewal_process(Step_process): 147 | """ 148 | parameters: 149 | T: T is a scipy "frozen" random variable object, from the scipy.stats library, that has the same distribution as the inter-arrival times 150 | i.e. t_{i+1} - t_{i} equal in distribution to T, where t_i are jump/event times. 151 | T must be a non-negative random variable, possibly constant. The constant case in included in this library, it 152 | can be called as Contant(c), where c is the interarrival time. 153 | 154 | J: is a scipy "frozen" random variable object that has the same distribution as the jump distribution. It can have any real support. 155 | Ex: for a poisson process, J is equal to 1 with probability 1, and we can use the Constant(1) class, 156 | but J is random for the compound poisson process. 157 | 158 | Note 1. endTime and endPosition are not allowed for this class. 159 | Note 2. If you are interested in Poisson or Compound poisson process, they are implemented in PyProcess. 160 | Those implementations are recommened to use versus using a Renewal_process. See Poisson_process 161 | and Compound_poisson_process. 162 | Ex: 163 | import scipy.stats as stats 164 | 165 | #create a standard renewal process 166 | renewal_ps= Renewal_process( J = Constant(3.5), T = stats.poisson(1), startTime = 0, startPosition = 0 ) 167 | 168 | """ 169 | 170 | 171 | def __init__(self, T, J, startPosition = 0, startTime = 0): 172 | 173 | super(Renewal_process, self).__init__(startTime = startTime,startPosition = startPosition) 174 | self.T = T 175 | self.J = J 176 | self.renewal_rate = 1/self.T.mean() 177 | 178 | def forward_recurrence_time_pdf(self,x): 179 | "the forward recurrence time RV is the time need to wait till the next event, after arriving to the system at a large future time" 180 | return self.renewal_rate*self.T.sf(x) 181 | 182 | def backwards_recurrence_time_pdf(self,x): 183 | "the backwards recurrence time is the time since the last event after arriving to the system at a large future time" 184 | return self.forward_recurrence_time_pdf(x) 185 | 186 | def forward_recurrence_time_cdf(self,x): 187 | "the forward recurrence time RV is the time need to wait till the next event, after arriving to the system at a large future time" 188 | return int.quad(self.forward_recurrence_time_pdf, 0, x) 189 | 190 | def backward_recurrence_time_cdf(self,x): 191 | "the backwards recurrence time is the time since the last event after arriving to the system at a large future time" 192 | return int.quad(self.forward_recurrence_time_pdf, 0, x) 193 | 194 | def spread_pdf(self,x): 195 | """the RV distributed according to the spread_pdf is the (random) length of time between the previous and next event/jump 196 | when you arrive at a large future time.""" 197 | try: 198 | return self.renewal_rate*x*self.T.pdf(x) 199 | except AttributeError: 200 | return self.renewal_rate*x*self.T.pmf(x) 201 | 202 | def _sample_path(self,time, N =1): 203 | """ 204 | parameters: 205 | time: an interable that returns floats OR if a single float, returns (path,jumps) up to time t. 206 | N: the number of paths to return. negative N returns an iterator of size N. 207 | returns: 208 | path: the sample path at the times in times 209 | jumps: the times when the process jumps 210 | 211 | Scales linearly with number of sequences and time. 212 | 213 | """ 214 | if N < 0: 215 | return self._iterator_sample_paths(time, -N) 216 | 217 | else: 218 | return self.__sample_path( time, N ) #np.asarray( [self.__sample_path(times) for i in xrange(N) ] ) 219 | 220 | def _iterator_sample_paths( self, times, N): 221 | 222 | for i in xrange(N): 223 | yield self.__sample_path(times) 224 | 225 | def __sample_path( self, times, N=1 ): 226 | 227 | if isinstance( times, int): #not the best way to do this. 228 | 229 | """user wants a path up to time times.""" 230 | def generate_jumps( x, J, start_position): 231 | n = x.shape[0] 232 | return start_position + J.rvs(n).cumsum() 233 | 234 | _times = self.sample_jumps(times,N) 235 | _path = np.asarray( map( lambda x: generate_jumps(x, self.J, self.startPosition), _times) ) 236 | return (_path, _times ) 237 | 238 | else: 239 | # times is an iterable, user requests "give me positions at these times." 240 | pass 241 | #TODO 242 | 243 | 244 | 245 | 246 | 247 | def __sample_jumps(self, t ): 248 | 249 | quantity_per_i = 10 250 | times = np.array([]) 251 | c = 0 #measure of size 252 | while True: 253 | c += quantity_per_i 254 | times = np.append( times, self.T.rvs(quantity_per_i) ) 255 | times = times[ times.cumsum() < t ] 256 | if times.shape[0] < c: 257 | #this will only happen if the list is truncated. 258 | return times.cumsum() 259 | 260 | def iterator_sample_jumps(self, t, N): 261 | 262 | for i in xrange(N): 263 | yield __sample_jumps(t) 264 | 265 | 266 | def _sample_jumps(self,t, N=1): 267 | """ 268 | Generate N simulations of jump times prior to some time t > startTime. 269 | 270 | parameters: 271 | t: the end of the processes. 272 | N: the number of samples to return, -N to return a generator of size N 273 | returns: 274 | An irregular numpy array, with first dimension N, and each element an np.array 275 | with random length. 276 | 277 | example: 278 | >> print renewal_ps.generate_sample_jumps( 1, N=2) 279 | np.array( [ [2,2.5,3.0], [1.0,2.0] ], dtype=0bject ) 280 | 281 | """ 282 | if N<0: 283 | return ( self.__sample_jumps(t) for i in xrange(-N) ) 284 | else: 285 | return np.asarray( [ self.__sample_jumps(t) for i in xrange(N) ] ) 286 | 287 | 288 | def _mean(self,t): 289 | #try: 290 | #return self.J.mean()*self.T.mean() 291 | #except: 292 | return self.J.mean()*self.mean_number_of_jumps(t) 293 | 294 | def _var(self,t): 295 | return self.mean_number_of_jumps(t)*self.J.var() 296 | 297 | def _sample_position(self,t, N = 1): 298 | """ 299 | Generate the position of the process at time t > startTime 300 | parameters: 301 | N: number of samples 302 | t: position to sample at. 303 | returns: 304 | returns a (n,) np.array 305 | """ 306 | v = np.zeros( N ) 307 | iv = np.ones( N, dtype=bool ) 308 | x = self.startPosition*np.ones( N) 309 | continue_ = True 310 | i = 1 311 | t = t - self.startTime #should be positive. 312 | size = v.shape[0] 313 | while continue_: 314 | n_samples = iv.sum() 315 | v[iv] += self.T.rvs( n_samples ) 316 | 317 | #if the next time is beyond reach, we do not add a new jump 318 | iv[ v >= t] = False 319 | n_samples = iv.sum() 320 | 321 | x[iv] += self.J.rvs( n_samples ) 322 | 323 | size = iv.sum() #any left? 324 | empty = size == 0 325 | 326 | i+=1 327 | if empty: 328 | continue_ = False 329 | 330 | if i > 10000: 331 | warnings.warn("The algorithm did not converge. Be sure 'T' is a non negative random \ 332 | variable, or set 't' to a smaller value.") 333 | break 334 | 335 | return x 336 | 337 | class Poisson_process(Renewal_process): 338 | """ 339 | This implements a Poisson process with rate parameter 'rate' (lambda was already taken!). 340 | Parameters: 341 | rate: float > 0 that define the inter-arrival rate. 342 | startTime: (default 0) The start time of the process 343 | startPosition: (default 0) The starting position of the process at startTime 344 | 345 | conditional processes: 346 | If the process is known at some future time, endPosition and endTime 347 | condition the process to arrive there. 348 | endTime: (default None) the time in the future the process is known at, > startTime 349 | endPosition: (default None) the position the process is at in the future. 350 | > startPosition and equal to startPosition + int. 351 | 352 | ex: 353 | 354 | #condition the process to be at position 5 at time 10. 355 | pp = Poisson_process( rate = 3, endTime = 10, endPosition = 5 ) 356 | 357 | """ 358 | 359 | def __init__(self,rate=1, startTime = 0, startPosition = 0, endPosition = None, endTime = None): 360 | 361 | assert rate > 0, "invalid parameter: rate parameter must be greater than 0." 362 | self.rate = rate 363 | self.Exp = stats.expon(1/self.rate) 364 | self.Con = Constant(1) 365 | super(Poisson_process,self).__init__(J=self.Con, T=self.Exp, startTime = startTime, startPosition = startPosition) 366 | self.Poi = stats.poisson 367 | if ( endTime != None ) and ( endPosition != None ): 368 | assert endTime > startTime, "invalid times: endTime > startTime." 369 | self.endTime = endTime 370 | self.endPosition = endPosition 371 | self.condition = True 372 | self.Bin = stats.binom 373 | elif ( endTime != None ) != ( endPosition != None ): 374 | raise Exception( "invalid parameter:", "Must include both endTime AND endPosition or neither" ) 375 | 376 | def _mean(self,t): 377 | """ 378 | recall that a conditional poisson process N_t | N_T=n ~ Bin(n, t/T) 379 | """ 380 | if not self.conditional: 381 | return self.startPosition + self.rate*(t-self.startTime) 382 | else: 383 | return self.endPosition*float(t)/self.endTime 384 | 385 | def _var(self,t): 386 | """ 387 | parameters: 388 | t: a time > startTime (and less than endTime if present). 389 | returns: 390 | a float. 391 | 392 | recall that a conditional poisson process N_t | N_T=n ~ Bin(n, t/T) 393 | 394 | """ 395 | if self.conditional: 396 | return self.endPosition*(1-float(t)/self.endTime)*float(t)/self.endTime 397 | else: 398 | return self.rate*(t-self.startTime) 399 | 400 | def __sample_jumps(self,t): 401 | 402 | if self.conditional: 403 | p = self.Bin.rvs(self.endPosition, t/self.endTime) 404 | else: 405 | p = self.Poi.rvs(self.rate*(t-self.startTime)) 406 | path=[] 407 | #array = [self.startTime+(T-self.startTime)*np.random.random() for i in xrange(p)] 408 | jump_times = self.startTime + (t - self.startTime)*np.random.random(p) 409 | jump_times.sort() 410 | #for i in xrange(p): 411 | # x+=1 412 | # path.append((array[i],x)) 413 | # i+=1 414 | return jump_times 415 | 416 | def _sample_jumps(self, t, N=1): 417 | """ 418 | Probably to be deleted. 419 | T: a float 420 | N: the number of sample paths 421 | Returns: 422 | an (N,) np.array of jump times. 423 | """ 424 | if N<0: 425 | return ( self.__sample_jumps(t) for i in xrange(-N) ) 426 | else: 427 | return np.asarray( [self.__sample_jumps(t) for i in xrange(N)] ) 428 | 429 | 430 | 431 | def _sample_position(self,t, N=1): 432 | if self.conditional: 433 | return self.Bin.rvs(self.endPosition-self.startPosition, float(t)/self.endTime, size = N)+self.startPosition 434 | else: 435 | return self.Poi.rvs(self.rate*(t-self.startTime), size = N)+self.startPosition 436 | 437 | 438 | class Marked_poisson_process(Renewal_process): 439 | """ 440 | This class constructs marked poisson process i.e. at exponentially distributed times, a 441 | Uniform(L,U) is generated. 442 | 443 | parameters: 444 | rate: the rate for the poisson process 445 | U: the upper bound of uniform random variate generation 446 | L: the lower bound of uniform random variate generation 447 | 448 | 449 | Note there are no other time-space constraints besides startTime 450 | """ 451 | def __init__(self,rate =1, L= 0, U = 1, startTime = 0): 452 | self.Poi = stats.poisson 453 | self.L = L 454 | self.U = U 455 | self.startTime = startTime 456 | self.rate = rate 457 | 458 | 459 | def generate_marked_process(self,T): 460 | 461 | p = self.Poi.rvs(self.rate*(T-self.startTime)) 462 | 463 | times = self.startTime + (T-self.startTime)*np.random.random( p ) 464 | path = self.L+(self.U-self.L)*np.random.random( p ) 465 | 466 | return ( path, times ) 467 | 468 | 469 | class Compound_poisson_process(Renewal_process): 470 | """ 471 | This process has expontially distributed inter-arrival times (i.e. 'rate'-poisson distributed number of jumps at any time), 472 | and has jump distribution J. 473 | parameters: 474 | 475 | J: a frozen scipy.stats random variable instance. It can have any support. 476 | 477 | Note: endTime and endPosition constraints will not be statisfied. 478 | 479 | Example: 480 | >> import stats.scipy as stats 481 | 482 | >> Nor = stats.norm(0,1) 483 | >> cmp = Compound_poisson_process(J = Nor) 484 | 485 | """ 486 | def __init__(self, J, rate = 1,startTime = 0, startPosition = 0): 487 | assert rate > 0, "Choose rate to be greater than 0." 488 | self.J = J 489 | self.rate = rate 490 | self.Exp = stats.expon(1./self.rate) 491 | super(Compound_poisson_process, self).__init__(J = J, T= T, startTime = startTime, startPosition = startPosition) 492 | self.Poi = stats.poisson 493 | 494 | def _mean(self,t): 495 | return self.startPosition + self.rate*(t-self.startTime)*self.J.mean() 496 | 497 | def _var(self,t): 498 | return self.rate*(t-self.startTime)*(self.J.var()-self.J.mean()**2) 499 | """ 500 | def __sample_jumps(self,T): 501 | p = self.Poi.rvs(self.rate*(T-self.startTime)) 502 | x = self.startPosition 503 | path=[] 504 | array = [self.startTime+(T-self.startTime)*np.random.rand() for i in range(p)] 505 | array.sort() 506 | for i in range(p): 507 | x+=self.J.rvs() 508 | path.append((array[i],x)) 509 | i+=1 510 | return path 511 | """ 512 | 513 | 514 | class Diffusion_process(object): 515 | # 516 | # Class that can be overwritten in the subclasses: 517 | # _sample_position(t, N=1) 518 | # _mean(t) 519 | # _var(t) 520 | # _sample_path(times, N=1) 521 | # 522 | # Class that should be present in subclasses: 523 | # 524 | # _transition_pdf(x,t,y) 525 | # 526 | # 527 | 528 | def __init__(self, startTime = 0, startPosition = 0, endTime = None, endPosition = None): 529 | 530 | self.conditional=False 531 | self.startTime = startTime 532 | self.startPosition = startPosition 533 | if ( endTime != None ) and ( endPosition != None ): 534 | assert endTime > startTime, "invalid parameter: endTime > startTime" 535 | self.endTime = endTime 536 | self.endPosition = endPosition 537 | self.conditional = True 538 | elif ( endTime != None ) != ( endPosition != None ): 539 | raise Exception( "invalid parameter:", "Must include both endTime AND endPosition or neither" ) 540 | 541 | 542 | def transition_pdf(self,t,y): 543 | self._check_time(t) 544 | "this method calls self._transition_pdf(x,t,y) in the subclass" 545 | try: 546 | if not self.conditional: 547 | return self._transition_pdf(self.startPosition, t-self.startTime, y) 548 | else: 549 | return self._transition_pdf(self.startPosition, t-self.startTime, y)*self._transition_pdf(y, self.endTime-t, self.endPosition)\ 550 | /self._transition_pdf(self.startPosition,self.endTime - self.startTime, self.endPosition) 551 | except AttributeError: 552 | raise AttributeError("Attn: transition density for process is not defined.") 553 | 554 | def expected_value(self,t, f= lambda x:x, N=1e6): 555 | """ 556 | This function calculates the expected value of E[ X_t | F ] where F includes start conditions and possibly end conditions. 557 | 558 | 559 | """ 560 | warnings.warn( "Performing Monte Carlo with %d simulations."%N) 561 | self._check_time(t) 562 | if not self.conditional: 563 | return f( self.sample_position(t, N) ).mean() 564 | else: 565 | #This uses a change of measure technique. 566 | """ 567 | sum=0 568 | self.conditional=False 569 | for i in range(N): 570 | X = self.generate_position_at(t) 571 | sum+=self._transition_pdf(X,self.endTime-t,self.endPosition)*f(X) 572 | self.conditional=True 573 | """ 574 | x = self.sample_position(t, N) 575 | mean = (self._transition_pdf(x,self.endTime-t,self.endPosition)*f(x)).mean() 576 | return mean/self._transition_pdf(self.startPosition, self.endTime-self.startTime, self.endPosition) 577 | 578 | def sample_position(self,t, N=1): 579 | """ 580 | if _get_position_at() is not overwritten in a subclass, this function will use euler scheme 581 | """ 582 | 583 | self._check_time(t) 584 | return self._sample_position(t, N) 585 | 586 | 587 | def mean(self,t): 588 | self._check_time(t) 589 | return self._mean(t) 590 | 591 | 592 | def var(self,t): 593 | self._check_time(t) 594 | return self._var(t) 595 | 596 | def sample_path(self,times, N = 1): 597 | self._check_time(times[0]) 598 | return self._sample_path(times, N) 599 | 600 | 601 | 602 | 603 | 604 | def _sample_path(self,times, N=1 ): 605 | return self.Euler_scheme(times, N) 606 | 607 | def _var(self,t, N = 1e6): 608 | """ 609 | var = SampleVarStat() 610 | for i in range(10000): 611 | var.push(self.generate_position_at(t)) 612 | return var.get_variance() 613 | """ 614 | return self.sample_position(t, n).var() 615 | 616 | def _mean(self,t): 617 | return self.expected_value( t) 618 | 619 | def _sample_position(self,t): 620 | return self.Euler_scheme(t) 621 | 622 | def _transition_pdf(self,x,t,y): 623 | warning.warn( "Attn: transition pdf not defined" ) 624 | 625 | def _check_time(self,t): 626 | if ttime>t: 646 | delta = time-t 647 | x += drift(x,t)*delta + np.sqrt(delta)*diffusion(x,t)*Nor.rvs() 648 | path.append((x,time)) 649 | delta=0.001 650 | j+=1 651 | time = times[j] 652 | else: 653 | x += drift(x,t)*delta + np.sqrt(delta)*diffusion(x,t)*Nor.rvs() 654 | t += delta 655 | 656 | return path 657 | 658 | def Milstein_Scheme(self, times, delta = 0.01 ): 659 | if not all( map( lambda x: hasattr(self, x), ( 'drift', 'diffusion', 'diffusion_prime' ) ) ): 660 | raise AttributeError("The process does not have 'drift', 'diffusion', or 'diffusion_prime' methods") 661 | 662 | pass 663 | 664 | 665 | 666 | 667 | def plot(self, times ,N=1, **kwargs): 668 | assert N >= 1, "N must be greater than 0." 669 | try: 670 | self._check_time(times[-1] ) 671 | plt.plot(times, self.sample_path(times, N).T, **kwargs ) 672 | except: 673 | self._check_time(times) 674 | times = np.linspace(self.startTime, times, 100) 675 | path = self.sample_path(times, N).T 676 | plt.plot(times, path, **kwargs ) 677 | plt.xlabel( "time, $t$") 678 | plt.ylabel( "position of process") 679 | plt.show() 680 | return 681 | 682 | class Wiener_process(Diffusion_process): 683 | """ 684 | This implements the famous Wiener process. I choose not to call it Brownian motion, as 685 | brownian motion is a special case of this with 0 drift and variance equal to t. 686 | 687 | dW_t = mu*dt + sigma*dB_t 688 | 689 | W_t ~ N(mu*t, sigma**2t) 690 | 691 | parameters: 692 | mu: the constant drift term, float 693 | sigma: the constant volatility term, float > 0 694 | """ 695 | 696 | def __init__(self, mu, sigma, startTime = 0, startPosition = 0, endPosition = None, endTime = None): 697 | super(Wiener_process,self).__init__(startTime, startPosition, endTime, endPosition) 698 | self.mu = mu 699 | self.sigma = sigma 700 | self.Nor = stats.norm() 701 | 702 | def _transition_pdf(self,x,t,y): 703 | return np.exp(-(y-x-self.mu*(t-self.startTime))**2/(2*self.sigma**2*(t-self.startTime)))\ 704 | /np.sqrt(2*pi*self.sigma*(t-self.startTime)) 705 | 706 | def _mean(self,t): 707 | if self.conditional: 708 | delta1 = t - self.startTime 709 | delta2 = self.endTime - self.startTime 710 | return self.startPosition + self.mu*delta1 + (self.endPosition-self.startPosition-self.mu*delta2)*delta1/delta2 711 | else: 712 | return self.startPosition+self.mu*(t-self.startTime) 713 | 714 | def _var(self,t): 715 | if self.conditional: 716 | delta1 = self.sigma**2*(t-self.startTime)*(self.endTime-t) 717 | delta2 = self.endTime-self.startTime 718 | return delta1/delta2 719 | else: 720 | return self.sigma**2*(t-self.startTime) 721 | 722 | def _sample_position(self,t, n=1): 723 | """ 724 | This incorporates both conditional and unconditional 725 | """ 726 | return self.mean(t) + np.sqrt(self.var(t))*self.Nor.rvs(n) 727 | 728 | def _sample_path(self,times, N = 1): 729 | 730 | path=np.zeros( (N,len(times)) ) 731 | path[ :, 0] = self.startPosition 732 | times = np.insert( times, 0,self.startTime) 733 | deltas = np.diff( times ) 734 | 735 | if not self.conditional: 736 | path += np.random.randn( N, len(times)-1 )*self.sigma*np.sqrt(deltas) + self.mu*deltas 737 | return path.cumsum(axis=1) 738 | 739 | 740 | else: 741 | """ 742 | Alternatively, this can be accomplished by sampling directly from a multivariate normal given a linear 743 | projection. Ie 744 | 745 | N | N dot 1 = endPosition ~ Nor( 0, Sigma ), where Sigma is a diagonal matrix with elements proportional to 746 | the delta. This only problem with this is Sigma is too large for very large len(times). 747 | 748 | """ 749 | T = self.endTime - self.startTime 750 | x = self.startTime 751 | for i, delta in enumerate( deltas ): 752 | x = x*(1-delta/T)+(self.endPosition - self.startPosition)*delta/T + self.sigma*np.sqrt(delta/T*(T-delta))*self.Nor.rvs(N) 753 | T = T - delta 754 | path[:,i] = x 755 | if abs(T -0)<1e-10: 756 | path[:,-1] = (self.endPosition - self.startPosition) 757 | path = path + self.startPosition 758 | return path 759 | 760 | def generate_max(self,t): 761 | pass 762 | 763 | def generate_min(self,t): 764 | pass 765 | 766 | def drift(t,x): 767 | return self.mu 768 | def diffusion(t,x): 769 | return self.sigma 770 | def diffusion_prime(t,x): 771 | return 0 772 | 773 | 774 | 775 | 776 | #have a Vasicek model that has an alternative parameterizatiom but essentially just maps to OU_process 777 | 778 | class OU_process(Diffusion_process): 779 | 780 | """ 781 | The Orstein-Uhlenbeck process is a mean reverting model that is often used in finance to model interest rates. 782 | It is also known as a Vasicek model. It is defined by the SDE: 783 | 784 | dOU_t = theta*(mu-OU_t)*dt + sigma*dB_t$ 785 | 786 | 787 | The model flucuates around its long term mean mu. mu is also a good starting Position of the process. 788 | 789 | There exists a solution to the SDE, see wikipedia. 790 | 791 | parameters: 792 | theta: float > 0 793 | mu: float 794 | sigma: float > 0 795 | 796 | 797 | """ 798 | def __init__(self, theta, mu, sigma, startTime = 0, startPosition = 0, endPosition = None, endTime = None ): 799 | assert sigma > 0 and theta > 0, "theta > 0 and sigma > 0." 800 | super(OU_process, self).__init__(startTime, startPosition, endTime, endPosition) 801 | self.theta = theta 802 | self.mu = mu 803 | self.sigma = sigma 804 | self.Normal = stats.norm() 805 | 806 | 807 | def _mean(self,t): 808 | if self.conditional: 809 | return super(OU_process,self)._mean(t) #TODO 810 | else: 811 | return self.startPosition*np.exp(-self.theta*(t-self.startTime))+self.mu*(1-np.exp(-self.theta*(t-self.startTime))) 812 | 813 | def _var(self,t): 814 | if self.conditional: 815 | return super(OU_process,self)._get_variance_at(t) 816 | else: 817 | return self.sigma**2*(1-np.exp(-2*self.theta*t))/(2*self.theta) 818 | 819 | def _transition_pdf(self,x,t,y): 820 | mu = x*np.exp(-self.theta*t)+self.mu*(1-np.exp(-self.theta*t)) 821 | sigmaSq = self.sigma**2*(1-np.exp(-self.theta*2*t))/(2*self.theta) 822 | return np.exp(-(y-mu)**2/(2*sigmaSq))/np.sqrt(2*pi*sigmaSq) 823 | 824 | 825 | def _sample_position(self,t): 826 | if not self.conditional: 827 | return self.get_mean_at(t)+np.sqrt(self.get_variance_at(t))*self.Normal.rvs() 828 | else: 829 | #this needs to be completed 830 | return super(OU_process,self)._generate_position_at(t) 831 | 832 | def sample_path(self,times, N= 1, return_normals = False): 833 | "the parameter Normals = 0 is used for the Integrated OU Process" 834 | if not self.conditional: 835 | path=np.zeros( (N,len(times)) ) 836 | times = np.insert( times, 0,self.startTime) 837 | path[ :, 0] = self.startPosition 838 | 839 | deltas = np.diff(times ) 840 | normals = np.random.randn( N, len(times)-1 ) 841 | x = self.startPosition*np.ones( N) 842 | sigma = np.sqrt(self.sigma**2*(1-np.exp(-2*self.theta*deltas))/(2*self.theta)) 843 | for i, delta in enumerate(deltas): 844 | mu = self.mu + np.exp(-self.theta*delta)*(x-self.mu) 845 | path[:, i] = mu + sigma[i]*normals[:,i] 846 | x = path[:,i] 847 | """ 848 | It would be really cool if there was a numpy func like np.cumf( func, array ) 849 | that applies func(next_x, prev_x) to each element. For example, lambda x,y: y + x 850 | is the cumsum, and lambda x,y: x*y is the cumprod function. 851 | """ 852 | if return_normals: 853 | return (path, normals ) 854 | else: 855 | return path 856 | 857 | else: 858 | #TODO 859 | path = bridge_creation(self,times) 860 | return path 861 | 862 | def drift(self, x, t): 863 | return self.theta*(self.mu-x) 864 | def diffusion( self, x,t ): 865 | return self.sigma 866 | 867 | class Integrated_OU_process(Diffusion_process): 868 | """ 869 | The time-integrated Orstein-Uhlenbeck process 870 | $IOU_t = IOU_0 + \int_0^t OU_s ds$ 871 | where $dOU_t = \theta*(\mu-OU_t)*dt + \sigma*dB_t, 872 | OU_0 = x0$ 873 | 874 | parameters: 875 | {theta:scalar > 0, mu:scalar, sigma:scalar>0, x0:scalar} 876 | 877 | modified from http://www.fisica.uniud.it/~milotti/DidatticaTS/Segnali/Gillespie_1996.pdf 878 | """ 879 | def __init__(self, parameters, time_space_constraints): 880 | super(Integrated_OU_process,self).__init__( time_space_constraints) 881 | self.OU = OU_process({"theta":parameters["theta"], "mu":parameters["mu"], "sigma":parameters["sigma"]}, {"startTime":time_space_constraints["startTime"], "startPosition":parameters["x0"]}) 882 | for p in parameters: 883 | setattr(self, p, float(parameters[p])) 884 | self.Normal = stats.norm() 885 | 886 | def _get_mean_at(self,t): 887 | delta = t - self.startTime 888 | if self.conditional: 889 | pass 890 | else: 891 | return self.startPosition + (self.x0-self.mu)/self.theta + self.mu*delta\ 892 | -(self.x0-self.mu)*np.exp(-self.theta*delta)/self.theta 893 | 894 | def _get_variance_at(self,t): 895 | delta = t - self.startTime 896 | if self.conditional: 897 | pass 898 | else: 899 | return self.sigma**2*(2*self.theta*delta-3+4*np.exp(-self.theta*delta) 900 | -2*np.exp(-2*self.theta*delta))/(2*self.sigma**3) 901 | 902 | 903 | def _generate_position_at(self,t): 904 | if self.conditional: 905 | pass 906 | else: 907 | return self.get_mean_at(t)+np.sqrt(self.get_variance_at(t))*self.Normal.rvs() 908 | 909 | def _transition_pdf(self,x,t,y): 910 | mu = x + (self.x0 - self.mu)/self.theta + self.mu*t - (self.x0-self.mu)*np.exp(-self.theta*t)/self.theta 911 | sigmaSq = self.sigma**2*(2*self.theta*t-3+4*np.exp(-self.theta*t)-2*np.exp(-2*self.theta*t))/(2*self.sigma**3) 912 | return np.exp(-(y-mu)**2/(2*sigmaSq))/np.sqrt(2*pi*sigmaSq) 913 | 914 | 915 | def generate_sample_path(self,times, returnUO = 0): 916 | "set returnUO to 1 to return the underlying UO path as well as the integrated UO path." 917 | if not self.conditional: 918 | xPath, listOfNormals = self.OU.generate_sample_path(times, 1) 919 | path = [] 920 | t = self.startTime 921 | y = self.startPosition 922 | for i, position in enumerate(xPath): 923 | delta = position[0]-t 924 | x = position[1] 925 | if delta != 0: 926 | #there is an error here, I can smell it. 927 | sigmaX = self.sigma**2*(1-np.exp(-2*self.theta*delta))/(2*self.theta) 928 | sigmaY = self.sigma**2*(2*self.theta*delta-3+4*np.exp(-self.theta*delta) 929 | -np.exp(-2*self.theta*delta))/(2*self.sigma**3) 930 | muY = y + (x-self.mu)/self.theta + self.mu*delta-(x-self.mu)*np.exp(-self.theta*delta)/self.theta 931 | covXY = self.sigma**2*(1+np.exp(-2*self.theta*delta)-2*np.exp(-self.theta*delta))/(2*self.theta**2) 932 | y = muY + np.sqrt(sigmaY - covXY**2/sigmaX)*self.Normal.rvs()+ covXY/np.sqrt(sigmaX)*listOfNormals[i] 933 | t = position[0] 934 | path.append((t,y)) 935 | if returnUO==0: 936 | return path 937 | else: 938 | return path, xPath 939 | else: 940 | path = bridge_creation(self,times) 941 | if returnUO==0: 942 | return path 943 | else: 944 | return path, xPath 945 | 946 | def _process2latex(self): 947 | 948 | return """$IOU_t = IOU_0 + \int_0^t OU_s ds 949 | \text{where} dOU_t = %.3f(%.3f-OU_t)dt + %.3fdB_t, 950 | OU_0 = x0 951 | """ %(self.theta, self.mu, self.sigma) 952 | 953 | class SqBessel_process(Diffusion_process): 954 | """ 955 | The (lambda0 dimensional) squared Bessel process is defined by the SDE: 956 | dX_t = lambda_0*dt + nu*sqrt(X_t)dB_t 957 | 958 | parameters: 959 | 960 | lambda_0: float, 961 | nu: float > 0 962 | 963 | Attn: startPosition and endPosition>0 964 | 965 | Based on R.N. Makarov and D. Glew's research on simulating squared bessel process. See "Exact 966 | Simulation of Bessel Diffusions", 2011. 967 | 968 | """ 969 | def __init__(self, lambda_0, nu, startTime = 0, startPosition = 1, endTime = None, endPosition = None ): 970 | super(SqBessel_process, self).__init__(startTime, startPosition, endTime, endPosition) 971 | try: 972 | self.endPosition = 4.0/parameters["nu"]**2*self.endPosition 973 | self.x_T = self.endPosition 974 | except: 975 | pass 976 | self.x_0 = 4.0/nu**2*self.startPosition 977 | self.nu = nu 978 | self.lambda0 = lambda_0 979 | self.mu = 2*float(self.lambda0)/(self.nu*self.nu)-1 980 | self.Poi = stats.poisson 981 | self.Gamma = stats.gamma 982 | self.Nor = stats.norm 983 | self.InGamma = IncompleteGamma 984 | 985 | 986 | 987 | def generate_sample_path(self,times,absb=0): 988 | """ 989 | absb is a boolean, true if absorbtion at 0, false else. See class' __doc__ for when 990 | absorbtion is valid. 991 | """ 992 | if absb: 993 | return self._generate_sample_path_with_absorption(times) 994 | else: 995 | return self._generate_sample_path_no_absorption(times) 996 | 997 | 998 | def _transition_pdf(self,x,t,y): 999 | try: 1000 | return (y/x)**(0.5*self.mu)*np.exp(-0.5*(x+y)/self.nu**2/t)/(0.5*self.nu**2*t)*iv(abs(self.mu),4*np.sqrt(x*y)/(self.nu**2*t)) 1001 | except AttributeError: 1002 | print("Attn: nu must be known and defined to calculate the transition pdf.") 1003 | 1004 | def _generate_sample_path_no_absorption(self, times): 1005 | "mu must be greater than -1. The parameter times is a list of times to sample at." 1006 | if self.mu<=-1: 1007 | print("Attn: mu must be greater than -1. It is currently %f."%self.mu) 1008 | return 1009 | else: 1010 | if not self.conditional: 1011 | x=self.startPosition 1012 | t=self.startTime 1013 | path=[] 1014 | for time in times: 1015 | delta=float(time-t) 1016 | try: 1017 | y=self.Poi.rvs(0.5*x/delta) 1018 | x=self.Gamma.rvs(y+self.mu+1)*2*delta 1019 | except: 1020 | pass 1021 | path.append((time,x)) 1022 | t=time 1023 | else: 1024 | path = bridge_creation(self, times, 0) 1025 | return path 1026 | return [(p[0],self.rescalePath(p[1])) for p in path] 1027 | 1028 | 1029 | 1030 | def _generate_sample_path_with_absorption(self,times): 1031 | "mu must be less than 0." 1032 | if self.mu>=0: 1033 | print("Attn: mu must be less than 0. It is currently %f."%self.mu) 1034 | else: 1035 | if not self.conditional: 1036 | path=[] 1037 | X=self.startPosition 1038 | t=self.startTime 1039 | tauEst=times[-1]+1 1040 | for time in times: 1041 | delta = float(time - t) 1042 | if tauEst>times[-1]: 1043 | p_a = gammaincc(abs(self.mu),0.5*X/(delta)) 1044 | if np.random.rand() < p_a: 1045 | tauEst = time 1046 | if time0: 1066 | print("mu must be less than 0. It is currently %f."%self.mu) 1067 | else: 1068 | X=self.startPosition 1069 | t=self.t_0 1070 | path=[] 1071 | FHT=self.startPosition/(2*self.Gamma.rvs(abs(self.mu))) 1072 | for time in times: 1073 | if time0} 1102 | 1103 | """ 1104 | def __init__(self, parameters, space_time_constraints): 1105 | super(CIR_process,self).__init__(space_time_constraints) 1106 | for p in parameters: 1107 | setattr(self, p, float(parameters[p])) 1108 | self.Normal = stats.norm() 1109 | #transform the space time positions 1110 | _space_time_constraints = {} 1111 | _space_time_constraints['startTime'] = self._time_transformation(space_time_constraints['startTime']) 1112 | _space_time_constraints['startPosition'] = self._inverse_space_transformation(space_time_constraints['startTime'], space_time_constraints['startPosition']) 1113 | try: 1114 | _space_time_constraints['endPosition'] = self._inverse_space_transformation(space_time_constraints['endTime'], space_time_constraints['endPosition']) 1115 | _space_time_constraints['endTime'] = self._time_transformation(space_time_constraints['endTime']) 1116 | except: 1117 | pass 1118 | self.SqB = SqBessel_process({"lambda0":parameters["lambda_0"], "nu":parameters["nu"]}, _space_time_constraints) #need to change start position for non-zero startTime 1119 | self.mu=self.SqB.mu 1120 | 1121 | def _process2latex(self): 1122 | return """ 1123 | $dCIR_t = (%.3f - %.3fCIR_t)dt + %.3f\np.sqrt(CIR_t)dB_t$ 1124 | """%(self.lambda_0, self.lambda_1, self.nu) 1125 | 1126 | def _transition_pdf(self,x,t,y): 1127 | return np.exp(self.lambda_1*t)*SqB._transition_pdf(x,self._time_transformation(t), np.exp(self.lambda_1*t)*y) 1128 | 1129 | def generate_sample_path(self, times, abs=0): 1130 | "abs is a boolean: true if desire nonzero probability of absorption at 0, false else." 1131 | #first, transform times: 1132 | transformedTimes = [self._time_transformation(t) for t in times] 1133 | path = self.SqB.generate_sample_path(transformedTimes,abs) 1134 | tpath = [self._space_transformation(times[i],p[1]) for i,p in enumerate(path) ] 1135 | path=[] 1136 | for i in xrange(len(tpath)): 1137 | path.append((times[i],tpath[i])) 1138 | return path 1139 | 1140 | def _generate_position_at(self,t): 1141 | t_prime = self._time_transformation(t) 1142 | x = self.SqB.generate_position_at(t_prime) 1143 | return self._space_transformation(t,x) 1144 | 1145 | def _time_transformation(self,t): 1146 | if self.lambda_1==0: 1147 | return t 1148 | else: 1149 | return (np.exp(self.lambda_1*t)-1)/self.lambda_1 1150 | 1151 | def _space_transformation(self,t,x): 1152 | return np.exp(-self.lambda_1*t)*x 1153 | 1154 | def _inverse_space_transformation(self,t,x): 1155 | return np.exp(self.lambda_1*t)*x 1156 | 1157 | def _inverse_time_transformation(self, t): 1158 | if self.lambda_1==0: 1159 | return t 1160 | else: 1161 | return np.log(self.lambda_1*t+1)/self.lambda_1 1162 | 1163 | def _get_mean_at(self,t): 1164 | pass 1165 | 1166 | def _get_variance_at(self,t): 1167 | pass 1168 | 1169 | class CEV_process(Diffusion_process): 1170 | """ 1171 | defined by: 1172 | $$dCEV = rCEVdt + \deltaCEV^{\beta+1}dW_t$$ 1173 | 1174 | parameters: 1175 | {r: scalar, delta:scalar>0, beta:scalar<0} #typically beta<=-1/2 1176 | 1177 | """ 1178 | 1179 | def __init__(self,parameters, time_space_constraints): 1180 | super(CEV_process,self).__init__(time_space_constraints) 1181 | for p in parameters: 1182 | setattr(self, p, parameters[p]) 1183 | time_space_constraints["startPosition"]=self.CEV_to_SqB(self.startPosition) 1184 | self.SqB = SqBessel_process({"lambda0":(2-1/self.beta), "nu":2}, time_space_constraints) 1185 | 1186 | def _process2latex(self): 1187 | return 1188 | """ 1189 | $dCEV_t = %.3fCEVdt + %.3fCEV^{\%.3f + 1}dB_t$ 1190 | """%(self.r, self.delta, self.beta) 1191 | 1192 | def _time_transform(self,t): 1193 | if self.r*self.beta==0: 1194 | return t 1195 | else: 1196 | return (np.exp(self.r*self.beta*2*t)-1)/(self.r*self.beta*2) 1197 | 1198 | def CEV_to_SqB(self,x): 1199 | return x**(-2*self.beta)/(self.delta*self.beta)**2 1200 | 1201 | def _scalar_space_transform(self,t,x): 1202 | return np.exp(self.r*t)*x 1203 | 1204 | def SqB_to_CEV(self,x): 1205 | ans = (self.delta**2*self.beta**2*x)**(-1/(2.0*self.beta)) 1206 | return ans 1207 | 1208 | 1209 | def generate_sample_path(self,times, abs=0): 1210 | if self.r==0: 1211 | SqBPath = self.SqB.generate_sample_path(times, abs) 1212 | return [(x[0],self.SqB_to_CEV(x[1])) for x in SqBPath] 1213 | else: 1214 | transformedTimes = [self._time_transform(t) for t in times] 1215 | SqBPath = self.SqB.generate_sample_path(transformedTimes, abs) 1216 | tempPath = [self.SqB_to_CEV(x[1]) for x in SqBPath] 1217 | return [(times[i], self._scalar_space_transform(times[i], p) ) for i,p in enumerate(tempPath)] 1218 | 1219 | def _generate_position_at(self,t): 1220 | if self.r==0: 1221 | SqBpos = self.SqB.generate_position_at(t) 1222 | return self.SqB_to_CEV(SqBpos) 1223 | else: 1224 | transformedTime = self._time_transform(t) 1225 | SqBpos = self.SqB.generate_position_at(transformedTime) 1226 | return self._scalar_space_transform(t,self.SqB_to_CEV(SqBpos)) 1227 | 1228 | class Periodic_drift_process(Diffusion_process): 1229 | """ 1230 | dX_t = psi*sin(X_t + theta)dt + dBt 1231 | 1232 | parameters: 1233 | {psi:scalar>0, theta:scalar>0} 1234 | 1235 | 1236 | This cannot be conditioned on start or end conditions. 1237 | Extensions to come. 1238 | """ 1239 | def __init__(self, parameters, space_time_constraints): 1240 | """Note that space-time constraints cannot be given""" 1241 | space_time_constraints = {"startTime":0, "startPosition":0} 1242 | super(Periodic_drift_process,self).__init__(space_time_constraints) 1243 | self.psi = parameters["psi"] 1244 | self.theta = parameters["theta"] 1245 | self._findBounds() 1246 | self.BB = Wiener_process({"mu":0, "sigma":1}, space_time_constraints) 1247 | self.Poi = Marked_poisson_process({"rate":1, "U":self.max, "L":self.min, "startTime":0}) #need to create marked poisson process class 1248 | self.Nor = stats.norm 1249 | self.Uni = stats.uniform() 1250 | 1251 | def _process2latex(self): 1252 | return """ 1253 | $dX_t = %.3f*sin(X_t + %.3f)dt + dB_t$ 1254 | """%(self.psi, self.theta) 1255 | 1256 | 1257 | 1258 | def __generate_sample_path(self,T,x): 1259 | #generates a path of length 2. This is for efficiency issues 1260 | self.BB.startPosition=x 1261 | while (True): 1262 | 1263 | 1264 | #produce a marked poisson process 1265 | markedProcess = self.Poi.generate_marked_process(T) 1266 | 1267 | #generate end point using an AR scheme 1268 | while (True): 1269 | N = x+ np.sqrt(T)*self.Nor.rvs() 1270 | U = self.Uni.rvs() 1271 | if U<=np.exp(-self.psi*cos(N-self.theta)+self.psi): 1272 | break 1273 | self.BB.endPosition = N 1274 | 1275 | #generate brownian bridge 1276 | try: 1277 | skeleton = self.BB.generate_sample_path([p[0] for p in markedProcess]) 1278 | except: 1279 | skeleton = [] 1280 | transformSkeleton = [self.phi(p[1]) for p in skeleton] 1281 | 1282 | #calculate indicators 1283 | I=1 1284 | for i in xrange(len(transformSkeleton)): 1285 | if transformSkeleton[i]>markedProcess[i][1]: 1286 | I=0 1287 | break 1288 | 1289 | #check indicators 1290 | if I==1: 1291 | return N, skeleton + [(T,N)] 1292 | 1293 | def generate_sample_path(self,times): 1294 | "currently will only return a random path before time T. Can be connected by brownian bridges" 1295 | #this algorithm uses the EA1 algorithm by Beskos and 1296 | # Roberts on exact simulation of diffusions. It's an AR algorithm. 1297 | # For some parameters, the probability of acceptance can be very low. 1298 | time = 0 1299 | endPoint = 0 1300 | skeleton=[] 1301 | T = times[-1] 1302 | while time0, "variance":scalar>0} 1538 | or 1539 | {"rate":scalar>0, "size":scalar>0} 1540 | 1541 | 1542 | Under the first parameterization: E[G(t)] = mean*t, Var(G(t)) = variance*t 1543 | Under the second parameterization: G(t+1) - G(t) ~ Gamma(rate, size) 1544 | where Gamma has pdf [size^(-rate)/gamma(rate)]*x^(rate-1)*np.exp(-x/size) 1545 | 1546 | see http://eprints.soton.ac.uk/55793/1/wsc03vg.pdf for details on this process. 1547 | 1548 | """ 1549 | 1550 | def __init__(self, parameters, time_space_constraints): 1551 | super(Gamma_process,self).__init__(time_space_constraints) 1552 | try: 1553 | self.mean = float(parameters["rate"]/parameters["size"]) 1554 | self.variance = float(self.mean/parameters["size"]) 1555 | except KeyError: 1556 | self.mean = float(parameters["mean"]) 1557 | self.variance = float(parameters["variance"]) 1558 | self.gamma = stats.gamma 1559 | self.beta = stats.beta 1560 | 1561 | def _generate_position_at(self,t): 1562 | if not self.conditional: 1563 | return self.startPosition + self.gamma.rvs(self.mean**2*(t-self.startTime)/self.variance)*self.mean/self.variance 1564 | else: 1565 | return self.startPosition + (self.endPosition-self.startPosition)*self.beta.rvs((t-self.startTime)/self.variance, (self.endTime-t)/self.variance) 1566 | 1567 | def _get_mean_at(self,t): 1568 | "notice the conditional is independent of self.mean." 1569 | if self.conditional: 1570 | return self.startPosition + (self.endPosition-self.startPosition)*(t- self.startTime)/(self.endTime - self.startTime) 1571 | else: 1572 | return self.startPosition + self.mean*(t-self.startTime) 1573 | 1574 | def _get_variance_at(self,t): 1575 | if self.conditional: 1576 | alpha = (t-self.startTime)/self.variance 1577 | beta = (self.endTime-t)/self.variance 1578 | return (self.endPosition-self.startPosition)**2(alpha*beta)/(alpha+beta)**2/(alpha+beta+1) 1579 | else: 1580 | return self.variance*(t-self.startTime) 1581 | 1582 | 1583 | def _generate_sample_path(self,times): 1584 | if not self.conditional: 1585 | t = self.startTime 1586 | x = self.startPosition 1587 | path=[] 1588 | for time in times: 1589 | delta = time - t 1590 | try: 1591 | g = self.gamma.rvs(self.mean**2*delta/self.variance)*self.variance/self.mean 1592 | x = x + g 1593 | except ValueError: 1594 | pass 1595 | t = time 1596 | path.append((t,x)) 1597 | return path 1598 | else: 1599 | x = self.startPosition 1600 | t = self.startTime 1601 | path=[] 1602 | for time in times: 1603 | delta1 = time -t 1604 | delta2 = self.endTime - time 1605 | if (delta1!=0 and delta2!=0): 1606 | b = (self.endPosition-x)*self.beta.rvs(delta1/self.variance,delta2/self.variance) 1607 | elif delta1 == 0: 1608 | b = 0 1609 | else: 1610 | b = self.endTime - x 1611 | x = x + b 1612 | t = time 1613 | path.append((t,x)) 1614 | return path 1615 | 1616 | def _transition_pdf(self,x,t,y): 1617 | return self.variance/self.mean*self.gamma.pdf((y-x)*self.mean/self.variance*t,self.mean**2/self.variance) 1618 | 1619 | 1620 | class Gamma_variance_process(Jump_Diffusion_process): 1621 | """ 1622 | The Gamma variance process is a brownian motion subordinator: 1623 | VG_t = mu*G_t(t,a,b) + sigma*B_{G_t(t,a,b)} 1624 | i.e. VG process is a time transformed brownian motion plus a scalar drift term. 1625 | It can also be represented by the difference of two gamma processes. 1626 | 1627 | Note: currently, if conditional, endPosition=0. Further extensions will be to make this nonzero. 1628 | 1629 | parameters: 1630 | {mu:scalar, sigma:scalar>0, variance:scalar>0} 1631 | or 1632 | {mu:scalar, sigma:scalar>0, rate:scalar>0} 1633 | *note the mean of the gamma process is 1, hence the reduction of parameters needed. 1634 | 1635 | The parameterization depends on the way you parameterize the underlying gamma process. 1636 | See http://www.math.nyu.edu/research/carrp/papers/pdf/VGEFRpub.pdf" 1637 | """ 1638 | 1639 | def __init__(self,parameters, space_time_constraints): 1640 | super(Gamma_variance_process,self).__init__(space_time_constraints) 1641 | try: 1642 | self.variance=v = float(parameters["variance"]) 1643 | except KeyError: 1644 | self.variance=v = 1/float(parameters["rate"]) 1645 | self.sigma=s = float(parameters["sigma"]) 1646 | self.mu= m = float(parameters["mu"]) 1647 | self.mu1 = 0.5*np.sqrt(m**2 + 2*s**2/v)+m/2 1648 | self.mu2 = 0.5*np.sqrt(m**2 + 2*s**2/v)-m/2 1649 | self.var1 = self.mu1**2*v 1650 | self.var2 = self.mu2**2*v 1651 | self.GamPos = Gamma_process({"mean":self.mu1,"variance":self.var1}, space_time_constraints) 1652 | self.GamNeg = Gamma_process({"mean":self.mu2, "variance":self.var2}, space_time_constraints) 1653 | 1654 | 1655 | def _get_mean_at(self,t): 1656 | return self.GamPos.get_mean_at(t)-self.GamNeg.get_mean_at(t) 1657 | 1658 | def _get_variance_at(self,t): 1659 | return self.GamPos.get_variance_at(t)+self.GamNeg.get_variance_at(t) 1660 | 1661 | def _generate_position_at(self,t): 1662 | x = self.GamPos.generate_position_at(t) 1663 | y = self.GamNeg.generate_position_at(t) 1664 | return x-y 1665 | 1666 | def _generate_sample_path(self,times): 1667 | path=[] 1668 | pathPos = self.GamPos.generate_sample_path(times) 1669 | pathNeg = self.GamNeg.generate_sample_path(times) 1670 | for i,time in enumerate(times): 1671 | path.append((time, pathPos[i][1] - pathNeg[i][1])) 1672 | return path 1673 | 1674 | def _transition_pdf(self,x,t,y): 1675 | alpha1=self.mu1**2*t/self.var1 1676 | beta1 = self.var1/self.mu2 1677 | alpha2 = self.mu2**2*t/self.var2 1678 | beta2 = self.var2/self.mu2 1679 | return np.exp(-(y-x)) 1680 | 1681 | 1682 | class Geometric_gamma_process(Jump_Diffusion_process): 1683 | """ 1684 | the geometric gamma process has the representation GG_0*np.exp(G_t) where G_t is a gamma process. 1685 | 1686 | parameters 1687 | {mu: scalar, sigma: scalar>0 } 1688 | The parameters refer to the parameters in the gamma process (mu = mean, sigma = variance) (see gamma_process documentation for more details). 1689 | 1690 | """ 1691 | 1692 | def __init__(self, parameters, space_time_constraints): 1693 | super(Geometric_gamma_process, self).__init__(space_time_constraints) 1694 | self.mu = float(parameters["mu"]) 1695 | self.sigma = float(parameters["sigma"]) 1696 | try: 1697 | self.gammaProcess = Gamma_process({"mean":self.mu, "variance":self.sigma}, {"startPosition":0, "startTime":0, "endTime":self.endTime, "endPosition":np.log(self.endPosition/self.startPosition)}) 1698 | except: 1699 | self.gammaProcess = Gamma_process({"mean":self.mu, "variance":self.sigma}, {"startPosition":0, "startTime":0}) 1700 | 1701 | 1702 | 1703 | def _transition_pdf(self,z,t,x): 1704 | "as this is a strictly increasing process, the condition x0 , b: >0 } 1732 | 1733 | Note: currently cannot be conditioned on endTime nor endPosition. 1734 | """ 1735 | 1736 | def __init__(self, parameters, timespace_constraints): 1737 | super(Inverse_Gaussian_process,self).__init__(timespace_constraints) 1738 | self.a = parameters['a'] 1739 | self.b = parameters['b'] 1740 | self.IG = InverseGaussian() 1741 | 1742 | 1743 | def _generate_position_at(self, t): 1744 | return self.startPosition + self.IG.rvs(self.a*(t - self.startTime),self.b) 1745 | 1746 | def _generate_sample_path(self, times): 1747 | x = self.startPosition 1748 | t = self.startTime 1749 | path = [] 1750 | for time in times: 1751 | delta = time - t 1752 | x += self.IG.rvs(self.a*delta, self.b) 1753 | t = time 1754 | path.append((t,x)) 1755 | return path 1756 | 1757 | def _get_mean_at(self,t): 1758 | return self.startPosition + self.a*t/self.b 1759 | 1760 | def _get_variance_at(self,t): 1761 | return (self.a*t/self.b)**3/(self.a*t) 1762 | 1763 | def _transition_pdf(self,z,t,x): 1764 | y = x-z 1765 | return self.a*(t-self.startTime)/(2*pi)*(y)**(-1.5)*np.np.exp(-0.5*( (self.a*(t-self.startTime))**2/y + self.b**2*y ) + self.a*(t-self.startTime)*self.b ) 1766 | 1767 | 1768 | class Normal_Inverse_Gaussian_process(Jump_Diffusion_process): 1769 | """This is a Brownian motion subordinated by a inverse gaussian process. 1770 | From http://finance.math.ucalgary.ca/papers/CliffTalk26March09.pdf 1771 | 1772 | paramters 1773 | {beta: scalar, alpha: |beta|0} 1774 | """ 1775 | 1776 | def __init__(self, parameters, timespace_constraints): 1777 | super(Normal_Inverse_Gaussian_process,self).__init__(timespace_constraints) 1778 | self.alpha = parameters['alpha'] 1779 | self.beta = parameters['beta'] 1780 | self.delta = parameters['delta'] 1781 | 1782 | b = self.delta*np.sqrt(self.alpha**2 - self.beta**2) 1783 | self.IGprocess = Inverse_Gaussian_process( {"a":1, "b":b}, {"startTime":0, "startPosition":self.startTime}) 1784 | self.BMprocess = Wiener_process({'mu':self.beta*self.delta**2, 'sigma':self.delta}, 1785 | {"startTime":self.startTime, 'startPosition':self.startPosition }) 1786 | 1787 | def _generate_position_at(self, t): 1788 | return self.BMprocess.generate_position_at(self.IGprocess.generate_position_at(t)) 1789 | 1790 | def _generate_sample_path(self, times): 1791 | p = self.IGprocess.generate_sample_path(times) 1792 | return self.BMprocess.generate_sample_path([x[1] for x in p]) 1793 | 1794 | def _get_mean_at(self,t): 1795 | return self.startPosition + self.delta*(t-self.startTime)*self.beta/(np.sqrt(self.alpha**2 - self.beta**2)) 1796 | 1797 | def _get_variance_at(self,t): 1798 | return self.delta*(t-self.startTime)*self.alpha**2/(np.sqrt(self.alpha**2 - self.beta**2))**3 1799 | 1800 | 1801 | def _transition_pdf(self,z,t,x): 1802 | y = np.sqrt(self.delta**2 - + (x-z)**2) 1803 | gamma = np.sqrt(self.alpha**2 - self.beta**2) 1804 | return self.alpha*self.delta*(t - self.startTime)*kn(1,self.alpha*y )*np.np.exp(self.delta*gamma+self.beta*(z-x) )/(pi*y) 1805 | 1806 | 1807 | 1808 | 1809 | 1810 | class Custom_process(object): 1811 | """ 1812 | This class is a user defined sum of processes. The parameters are classes from this module. 1813 | Ex: 1814 | WP = Wiener_process{parametersWP, spaceTimeConstraintsWP} 1815 | PP = Poisson_process{parametersPP, space_time_constraintsPP} 1816 | Custom = Custom_process( WP, PP ) 1817 | 1818 | Custom.get_mean_at(10) 1819 | 1820 | 1821 | """ 1822 | def __init__(self, *args): 1823 | self.processes = [] 1824 | for arg in args: 1825 | self.processes.append(arg) 1826 | 1827 | def get_mean_at(self,t): 1828 | sum=0 1829 | for p in self.processes: 1830 | sum+=p.get_mean_at(t) 1831 | return sum 1832 | 1833 | def get_variance_at(self,t): 1834 | sum=0 1835 | for p in self.processes: 1836 | sum+=p.get_variance_at(t) 1837 | return sum 1838 | 1839 | def generate_position_at(self,t): 1840 | sum=0 1841 | for p in self.processes: 1842 | sum+=p.generate_position_at(t) 1843 | return sum 1844 | 1845 | def generate_sample_path(self,times, *args): 1846 | "returns an array of a path, not an immutable list!" 1847 | path = [[t,0] for t in times] 1848 | for i in xrange(len(self.processes)): 1849 | try: 1850 | tempPath = self.processes[i].generate_sample_path(times, args[i] ) 1851 | except: 1852 | tempPath = self.processes[i].generate_sample_path(times) 1853 | for k in xrange(len(tempPath)): 1854 | path[k][1]+=tempPath[k][1] 1855 | return path 1856 | 1857 | 1858 | 1859 | 1860 | 1861 | 1862 | #------------------------------------------------------------------------------ 1863 | # auxilary classes and functions 1864 | 1865 | class Constant(object): 1866 | def __init__(self,c): 1867 | self.c=c 1868 | 1869 | def mean(self): 1870 | return self.c 1871 | def var(self): 1872 | return 0 1873 | def rvs(self, n): 1874 | return self.c*np.ones(n) 1875 | 1876 | def cdf(self,x): 1877 | if x0) 1913 | i=1 1914 | N = len(times) 1915 | sample_path.append(forward[0]) 1916 | while (i0) ): 1917 | sample_path.append(forward[i]) 1918 | i+=1 1919 | 1920 | if i != N-1: #an intersection was found 1921 | k=0 1922 | while(N-1-i-k>=0): 1923 | sample_path.append((times[i+k],backward[N-1-i-k][1])) 1924 | k+=1 1925 | process.conditional = True 1926 | 1927 | return sample_path 1928 | 1929 | 1930 | 1931 | def reverse_times(process, times): 1932 | reverse_times=[] 1933 | for time in reversed(times): 1934 | reverse_times.append(process.endTime - time - process.startTime) 1935 | return reverse_times 1936 | 1937 | def transform_path(path,f): 1938 | "accepts a path, ie [(t,x_t)], and transforms it into [(t,f(x)]." 1939 | return [(p[0],f(p[1])) for p in path] 1940 | 1941 | class SampleVarStat(object): 1942 | def __init__(self): 1943 | self.S=0 1944 | self.oldM=0 1945 | self.newM=0 1946 | self.k=1 1947 | 1948 | 1949 | 1950 | def push(self,x): 1951 | if self.k==0: 1952 | self.S=0 1953 | self.oldM=x 1954 | else: 1955 | self.newM = self.oldM+(x-self.oldM)/self.k 1956 | self.S+=(x-self.oldM)*(x-self.newM) 1957 | self.oldM=self.newM 1958 | 1959 | self.k+=1 1960 | 1961 | def get_variance(self): 1962 | return self.S/(self.k-1) 1963 | 1964 | class IncompleteGamma(object): 1965 | "defined on negative integers. This is untested for accuracy" 1966 | "Used in Bessel process simulation." 1967 | def __init__(self,shape=None,scale=None): 1968 | self.shape = float(shape) 1969 | self.scale = float(scale) 1970 | self.Uni = stats.uniform 1971 | 1972 | def pdf(self,n,shape,scale): 1973 | inc = gammainc(shape,scale) #incomplete gamma function 1974 | return np.np.exp(-scale)*sp.power(scale, n+shape)/(gamma(n+1+shape)*inc) 1975 | 1976 | def cdf(self,n, shape,scale): 1977 | sum=0 1978 | for i in xrange(n+1): 1979 | sum+=self.pdf(i,shape,scale) 1980 | return sum 1981 | 1982 | def rvs(self,shape,scale): 1983 | "uses a inversion method: the chop-down-search starting \ 1984 | at the mode" 1985 | pos = mode = float(max(0,int(shape-scale))) 1986 | U = self.Uni.rvs() 1987 | sum = self.pdf(mode,shape,scale) 1988 | ub = mode+1 1989 | lb = mode-1 1990 | Pub = sum*scale/(mode+1+shape) 1991 | Plb = sum*(mode+shape)/scale 1992 | while sumPub and lb>=0: 1994 | sum+=Plb 1995 | pos = lb 1996 | Plb = (lb+shape)/scale*Plb 1997 | lb-=1 1998 | else: 1999 | sum+=Pub 2000 | pos= ub 2001 | Pub = scale/(ub+1+shape)*Pub 2002 | ub+=1 2003 | return float(pos) 2004 | 2005 | def rvsII(self,shape,scale): 2006 | U = self.Uni.rvs() 2007 | pos = 0 2008 | sum = self.pdf(pos,shape,scale) 2009 | while sum