├── .gitignore ├── README.md ├── poisson_process.py ├── p_increase_midprice_laplace.py ├── bdp.py ├── model.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smobd 2 | Replication of A Stochastic Model for Order Book Dynamics by Cont, Stoikov, and Talreja, 2010 3 | -------------------------------------------------------------------------------- /poisson_process.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | np.random.seed(0) 5 | 6 | current_value = 0 7 | pp = [current_value,] 8 | 9 | for i in range(25): 10 | current_value += np.random.poisson(0.5) 11 | pp.append(current_value) 12 | 13 | # example: 14 | # http://www.math.uchicago.edu/~may/VIGRE/VIGRE2010/REUPapers/Mcquighan.pdf page 8 15 | plt.plot(pp) 16 | plt.show() 17 | -------------------------------------------------------------------------------- /p_increase_midprice_laplace.py: -------------------------------------------------------------------------------- 1 | from mpmath import * 2 | from utils import get_lambda 3 | from utils import get_mu 4 | from utils import get_theta 5 | import sys 6 | sys.setrecursionlimit(11000) 7 | 8 | INF = 10*6 9 | 10 | # implements eq 15 11 | def p_increase_midprice_laplace(a, b, S): 12 | def a_n(k): 13 | return( -get_lambda(S) * (get_mu()+k*get_theta(S)) ) 14 | 15 | def b_n(k, s): 16 | return( get_lambda(S)+get_mu()+k*get_theta(S)+s ) 17 | 18 | def Phi(k, s): 19 | if k == INF: 20 | u = 0 21 | else: 22 | u = Phi(k+1, s) 23 | 24 | return ( a_n(k) / ( b_n(k, s) + u ) ) 25 | 26 | # eq 18 27 | def f_hat(j, s): 28 | result = (-1/get_lambda(S))**j 29 | for i in range(1, j+1): 30 | result *= Phi(i, s) 31 | return (result) 32 | 33 | # eq 20 34 | def fp(s): 35 | return (1/s * f_hat(a, s) * f_hat(b, -s)) 36 | 37 | mp.dps = 15; mp.pretty = True 38 | return ( invertlaplace(fp,0.0001) ) # 0 results with division by zero error? 39 | 40 | print(p_increase_midprice_laplace(1,10,1)) 41 | -------------------------------------------------------------------------------- /bdp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | ''' 3 | Use Gillespie algorithm to implement death-birth process 4 | # Reference: https://pdfs.semanticscholar.org/f70e/d6c71bc4a7fa5f4584b8616187b73ade644d.pdf 5 | ''' 6 | 7 | 8 | EVENT_NUM=10000 9 | 10 | state = 0 11 | 12 | # birth rate 13 | lam = 2 14 | 15 | # death rate 16 | mu = 3 17 | 18 | clock = 0 19 | 20 | all_states = {} 21 | 22 | for event in range(EVENT_NUM): 23 | if state == 0: 24 | # no death transition possible when state = 0 25 | sum_rates = lam 26 | else: 27 | sum_rates = mu+lam 28 | 29 | # pick next event time 30 | tau = np.random.exponential(1/sum_rates) 31 | 32 | if state not in all_states: 33 | all_states[state] = tau 34 | else: 35 | all_states[state] += tau 36 | 37 | # pick next event type 38 | if np.random.uniform() < (lam/sum_rates): 39 | #birth 40 | state += 1 41 | else: 42 | #death 43 | state -= 1 44 | 45 | clock += tau 46 | 47 | print(all_states) 48 | 49 | all_tau = sum(all_states.values()) 50 | 51 | for state in all_states: 52 | print('state %s probability %s' %(state, all_states[state]/all_tau)) 53 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from utils import random_init_X 3 | from utils import simulate_order_book 4 | import random 5 | import matplotlib.pyplot as plt 6 | import sys 7 | import json 8 | 9 | n=500 10 | 11 | random.seed(0) 12 | np.random.seed(0) 13 | 14 | X=random_init_X(n) 15 | #X=np.zeros((n)) 16 | 17 | print(X) 18 | 19 | (all_states, Q_i, RV) = simulate_order_book(pow(10,6), X) 20 | all_tau = sum(all_states.values()) 21 | 22 | states_with_highest_p_num = 5 23 | states_with_highest_p = [(0,0)]*states_with_highest_p_num 24 | 25 | for state in all_states: 26 | print('state %s probability %s' %(state, all_states[state]/all_tau)) 27 | 28 | if states_with_highest_p[0][1] < all_states[state]/all_tau: 29 | states_with_highest_p.pop() 30 | states_with_highest_p.insert( 0, (state, all_states[state]/all_tau) ) 31 | 32 | # state = np.array(json.loads(state)) 33 | 34 | print('Q_i %s' %Q_i) 35 | print('RV %s' %RV) 36 | 37 | print('states_with_highest_p') 38 | for s in states_with_highest_p: 39 | print(s) 40 | sys.stdout.flush() 41 | 42 | plt.plot(range(1,len(Q_i)), Q_i[1:]) 43 | plt.title('average number of orders') 44 | 45 | plt.show() 46 | 47 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | import json 4 | import math 5 | 6 | # TODO: add admissibility check 7 | 8 | def random_init_X(n): 9 | # Generate sensible random X 10 | 11 | sensible_range = int(n/5) 12 | 13 | # pick midprice 14 | # force midprice to be in middle of range 15 | p_m = random.randint(int(n/2 - sensible_range), int(n/2 + sensible_range)) 16 | 17 | # pick sensible spread size 18 | p_s = 1 #random.randint(1,sensible_range) 19 | 20 | min_order_size = 0 21 | max_order_size = 2 22 | orders = [] 23 | for i in range(2*sensible_range): 24 | orders.append(int(random.random()*(max_order_size-min_order_size)+min_order_size)) 25 | orders = np.array(orders) 26 | 27 | bid_orders = -1 * orders[0:int(len(orders)/2)] 28 | ask_orders = orders[int(len(orders)/2):] 29 | 30 | X=np.zeros((n)) 31 | X[p_m-sensible_range-p_s:p_m-p_s] = bid_orders 32 | X[p_m+p_s:p_m+sensible_range+p_s] = ask_orders 33 | return (X) 34 | 35 | 36 | class UnknownPriceException(Exception): 37 | pass 38 | 39 | 40 | def get_lambda(i): 41 | if i == 1: 42 | return 1.85 43 | elif i == 2: 44 | return 1.51 45 | elif i == 3: 46 | return 1.09 47 | elif i == 4: 48 | return 0.88 49 | elif i == 5: 50 | return 0.77 51 | else: 52 | return 0.001 53 | # if i > 30: 54 | # return 0.0001 55 | # else: 56 | # return 0.77-((i-5)*((0.77-0.001)/25)) 57 | #raise UnknownPriceException('unknown i') 58 | 59 | 60 | def get_theta(i): 61 | if i == 1: 62 | return 0.71 63 | elif i == 2: 64 | return 0.81 65 | elif i == 3: 66 | return 0.68 67 | elif i == 4: 68 | return 0.56 69 | elif i == 5: 70 | return 0.47 71 | else: 72 | return 0.001 73 | # if i > 30: 74 | # return 0.0001 75 | # else: 76 | # return 0.47-((i-5)*((0.47-0.001)/25)) 77 | #raise UnknownPriceException('unknown i') 78 | 79 | 80 | def get_mu(): 81 | return 0.94 82 | 83 | 84 | def get_k(): 85 | return 1.92 86 | 87 | 88 | def get_alpha(): 89 | return 0.52 90 | 91 | 92 | def get_mid_price(X): 93 | [p_A, p_B] = get_ask_bid_price(X) 94 | # rounding with int ok? 95 | return (p_A + p_B) / 2 96 | 97 | 98 | def get_ask_bid_price(X): 99 | # returns best ask and bid price given state X 100 | # p_A and p_B are defined as indices of X 101 | p_A = np.where(X>0)[0][0] 102 | p_B = np.where(X<0)[0][-1] 103 | return [p_A, p_B] 104 | 105 | 106 | def get_rates(X): 107 | # sum all possible event rates 108 | # TODO: convert nnz_rates to dictionary 109 | 110 | [p_A, p_B] = get_ask_bid_price(X) 111 | n = len(X) 112 | 113 | nnz_rates = [] 114 | sum_rates = 0 115 | 116 | # limit buy order 117 | # x -> x^(p-1) 118 | for relative_p in range(1, p_A+1): 119 | rate = get_lambda(relative_p) 120 | sum_rates += rate 121 | if rate: 122 | nnz_rates.append(('limitbuy', p_A-relative_p, rate)) 123 | 124 | # limit sell order 125 | # x -> x^(p+1) 126 | for relative_p in range(1, n-p_B): 127 | rate = get_lambda(relative_p) 128 | sum_rates += rate 129 | if rate: 130 | nnz_rates.append(('limitsell', p_B+relative_p, rate)) 131 | 132 | # market buy order 133 | # x -> x^(p_A(t)+1) 134 | sum_rates += get_mu() 135 | nnz_rates.append(('marketbuy', p_A, get_mu())) 136 | 137 | # market sell order 138 | # x -> x^(p_B(t)-1) 139 | sum_rates += get_mu() 140 | nnz_rates.append(('marketsell', p_B, get_mu())) 141 | 142 | # cancel limit buy order 143 | # x -> x^(p+1) 144 | for relative_p in range(1, p_A+1): 145 | rate = get_theta(relative_p) * abs(X[p_A-relative_p]) 146 | sum_rates += rate 147 | if rate: 148 | nnz_rates.append(('cancellimitbuy', p_A-relative_p, rate)) 149 | 150 | # cancel limit sell order 151 | # x -> x^(p+1) 152 | for relative_p in range(1, n-p_B): 153 | rate = get_theta(relative_p) * abs(X[p_B+relative_p]) 154 | sum_rates += rate 155 | if rate: 156 | nnz_rates.append(('cancellimitsell', p_B+relative_p, rate)) 157 | 158 | return (sum_rates, nnz_rates) 159 | 160 | 161 | def print_execution_error(error_str, event, state): 162 | [p_A, p_B] = get_ask_bid_price(state) 163 | return ('%s p_A %s p_B %s event %s state \n%s' %(error_str, p_A, p_B, event, state)) 164 | 165 | def execute_event(event, state, order_size=1): 166 | event_type = event[0] 167 | event_price = event[1] 168 | 169 | new_state = state 170 | if event_type == 'limitbuy': 171 | assert state[event_price] <= 0, print_execution_error('limit buy orders must not arrive at positive quotes', event, state) 172 | new_state[event_price] -= order_size 173 | elif event_type == 'limitsell': 174 | assert state[event_price] >= 0, print_execution_error('limit sell orders must not arrive at negative quotes', event, state) 175 | new_state[event_price] += order_size 176 | elif event_type == 'marketbuy': 177 | assert state[event_price] > 0, print_execution_error('best ask price quotes must be positive', event, state) 178 | new_state[event_price] -= order_size 179 | elif event_type == 'marketsell': 180 | assert state[event_price] < 0, print_execution_error('best sell price quotes must be negative', event, state) 181 | new_state[event_price] += order_size 182 | elif event_type == 'cancellimitbuy': 183 | assert state[event_price] < 0, print_execution_error('cancel limit buy orders must not arrive at positive quotes', event, state) 184 | new_state[event_price] += order_size 185 | elif event_type == 'cancellimitsell': 186 | assert state[event_price] > 0, print_execution_error('cancel limit sell orders must not arrive at negative quotes', event, state) 187 | new_state[event_price] -= order_size 188 | else: 189 | raise Exception('programming error: unknown event_type for event: %s' %event) 190 | 191 | return new_state 192 | 193 | 194 | def simulate_order_book(event_counter, initial_X): 195 | n = len(initial_X) 196 | state = initial_X 197 | state_str = json.dumps(list(initial_X)) 198 | 199 | clock = 0 200 | all_states = {} 201 | Q_i = [0]*n 202 | Q_i_counter = 0 203 | 204 | limit_order_counter = 0 205 | cancel_limit_order_counter = 0 206 | market_order_counter = 0 207 | all_sum_rates = [] 208 | all_taos = [] 209 | p_m_current = get_mid_price(initial_X) 210 | volatility_price_change_num = 370 211 | RV = 0 212 | for event_index in range(event_counter): 213 | (sum_rates, nnz_rates) = get_rates(state) 214 | all_sum_rates.append(sum_rates) 215 | 216 | # pick next event time 217 | # tau = np.random.exponential(1/sum_rates) 218 | tau = 1/sum_rates * np.log(1/np.random.uniform()) 219 | all_taos.append(tau) 220 | 221 | if state_str not in all_states: 222 | all_states[state_str] = tau 223 | else: 224 | all_states[state_str] += tau 225 | 226 | # pick next event type 227 | rand_event_p = np.random.uniform() * sum_rates 228 | event = None 229 | for nnz_rate in nnz_rates: 230 | rand_event_p -= nnz_rate[2] 231 | if rand_event_p <= 0: 232 | event = nnz_rate 233 | break 234 | 235 | assert event, 'event can not be none' 236 | 237 | [p_A, p_B] = get_ask_bid_price(state) 238 | event_type = event[0] 239 | event_price = event[1] 240 | if event_type in ['limitbuy', 'cancellimitbuy']: 241 | best_quote = p_A 242 | assert best_quote > event_price, 'limitbuy or cancellimitbuy quote can not have price %s larger than best ask price %s event %s' %(event_price, p_A, event) 243 | elif event_type in ['limitsell', 'cancellimitsell']: 244 | best_quote = p_B 245 | assert best_quote < event_price, 'limitsell or cancellimitsell quote can not have price %s less than best bid price %s event %s' %(event_price, p_A, event) 246 | else: 247 | best_quote = None 248 | 249 | if event_type in ['cancellimitbuy', 'cancellimitsell']: 250 | cancel_limit_order_counter += 1 251 | elif event_type in ['limitbuy', 'limitsell']: 252 | limit_order_counter += 1 253 | elif event_type in ['marketbuy', 'marketsell']: 254 | market_order_counter += 1 255 | 256 | 257 | #sum should be in integer groups of tau 258 | 259 | if best_quote: 260 | #Q_i[abs(best_quote-event_price)] += abs(state[event_price]) 261 | for i in range(1, 30): 262 | # number of buy orders at a distance i from the ask 263 | q_i_B = abs(state[p_A-i]) 264 | # number of sell orders at a distance i from the bid 265 | q_i_A = state[p_B+i] 266 | 267 | #state_debug_str = '' 268 | # for s_i, s in enumerate(state): 269 | # state_debug_str += '%s: %s\n' %(s_i, s) 270 | assert q_i_B >= 0 and q_i_A >= 0, 'q_i_A and q_i_B must be larger than zero! i %s p_A %s p_B %s q_i_B %s q_i_A %s event %s state %s' %(i, p_A, p_B, q_i_B,q_i_A,event, state) 271 | 272 | Q_i[i] += q_i_B+q_i_A 273 | Q_i_counter += 2 274 | 275 | state = execute_event(event, state) 276 | state_str = json.dumps(list(state)) 277 | 278 | p_m_new = get_mid_price(state) 279 | if p_m_new != p_m_current: 280 | if volatility_price_change_num != 0: 281 | volatility_price_change_num -= 1 282 | RV += pow( np.log( p_m_new / p_m_current ), 2) 283 | p_m_current = p_m_new 284 | 285 | clock += tau 286 | 287 | if volatility_price_change_num == 0: 288 | print( 'volatility_price_change_num == 0 break event_index %s' %event_index ) 289 | #break 290 | 291 | for q_i, q in enumerate(Q_i): 292 | Q_i[q_i] /= Q_i_counter # (limit_order_counter+cancel_limit_order_counter) 293 | 294 | print('limit_order_counter %s cancel_limit_order_counter %s market_order_counter %s' %(limit_order_counter, cancel_limit_order_counter, market_order_counter)) 295 | print('all_sum_rates %s' %all_sum_rates) 296 | print('all_taos %s' %all_taos) 297 | print('volatility_price_change_num %s' %volatility_price_change_num) 298 | return (all_states, Q_i, np.sqrt(RV)) 299 | --------------------------------------------------------------------------------