A simple limit order book

This section covers the limit order book model from [CI02]. This model was later extended in [CIP09], but for simplicity we will stay with the base model from the earlier paper. The objective in these papers was to build a simple model which could allow for the analysis of market micro structure and order book dynamics. The models endow the traders with some predictive capabilities, but they are near zero intelligence in their behavior.

Electronic limit order books form the core structure of much modern financial market trading. As an institution they are both an amazingly simple mechanism for getting a market into equilibrium, but they are also surprisingly rich and complex in terms of their dynamics. Beyond markets for financial instruments, they also are a key part in much of experimental economics, and they are referred to in several other sections in these notes: Generating Bubbles: Duffy and Unver(2006), and, Zero Intelligence Traders: Gode and Sunder (1993).

The agent-based world has relatively few examples of limit order books as compared with other market structures.

Agent forecasts

Agents in the model start by building simple price forecasts. A major structural difference in this model is that each agent is built as a kind of composite forecaster, combining forecasts that use fundamental, chartist, and noise trader components to forecast future price changes (returns).

They are built from a simple weighted average of the three components.

\[\hat r^i_{t} = \frac{1}{g_f^i + g_m^i + g_n^i} [ g_f^i (\frac{(p_t^f-p_t}{p_t}) + g_m^i\bar r_t^i + g_n^i\epsilon_t]\]

It is possible that this composite agent is made up from a population, or actually holds a kind of weighted forecasting system inside its brain. The actual mechanism is irrelevant to the model. The noise component of the forecast is given by \(\epsilon_t\) which is independent over time with variance \(\sigma^2_\epsilon\).

The forecast weights are drawn from normal distributions.

\[\begin{split}g_1^i \sim |N(0,\sigma_f)| \\ g_2^i \sim N(0,\sigma_m) \\ n^i \sim N(0,\sigma_n)\end{split}\]

The trend following, or momentum part is given by,

\[\bar r_t^i = \frac{1}{L_i} \sum_{j=1}^{\tau^i} r_{t-j} =\frac{1}{L^i} \sum_{j=1}^{L^i} \frac{p_{t-j}-p_{t-j-1}}{p_{t-j-1}}\]

with each agent using a possibly different value of \(L_i\) drawn uniformly in \([L_{min},L_{max}]\).

The agent now comes to the market with this expectational machinery in place and converts this to a price forecast using

\[\hat p_{t}^i = p_t \exp(\hat r^i)\]

The order book considers only orders at discrete price points. All prices will eventually be rounded to the nearest \(\Delta\) value.

Limit order determination

Though the traders in this market are somewhat purposeful, they still behave subject to a lot of randomness. They do not interact strategically with the order book, or the state of the market, but they do use their previous price forecast to determine orders to place on the book. Agents fix their quantities at one share of the risky asset. If they forecast a price increase with

\[\hat p_{t}^i > p_t\]

then a bid or order to buy is generated. The agent choses a price between the current price and the forecast price using,

\[b_t^i = \hat p^i_{t} (1-k^i).\]

The value \(k^i\) is a random value used to add noise to the trader’s beliefs and smooth out the market. It is random across agents, but constant through time. It is drawn from a uniform distribution over \([0,k_{max}]\). If this bid is larger than the lowest asking price on the book then a trade takes place, and the agent purchases at the best (lowest) ask, removing that order from the book. If \(b_t^i\) is smaller than the lowest asking price then it is added to the bid side of the limit order book, replacing the best bid if it is larger than that.

If the trader’s forecast price is below the current price,

\[\hat p_{t}^i < p_t\]

then an ask or order to sell is generated using

\[a_t^i = \hat p^i_{t} (1+k^i).\]

Again, \(k^i\) is a random component specific to agent i. Updates to the limit order book are again determined by how this sits relative to the set of current bids. If it falls below the best bid then a trade takes place at the best bid. If it is larger than the best bid then the order is placed on the ask side of the book, replacing the best ask when it is lower then the current best ask.

Market dynamics

At each tick step (labeled by t) an agent is chosen at random. The previously described forecasting and order determination takes place, and when a trade occurs the current price, \(p_t\) is updated to the traded price. When no trade occurs, the current price is set to the midpoint of the best bid and ask prices.

The book is then cleared of expired orders. Orders are considered expired if they have been on the book for longer than \(\tau\) periods. Therefore, all orders j with

\[t-t_j > \tau\]

are removed from the book, where \(t_j\) is the date order j was entered.

Most summary statistics and plotting are done at a lower frequency than trading. This gives the simulation of a kind of trading period in which trades take place. It might be an hour, a day or a week, but this is left open in the model. In all results presented here prices and returns will correspond to sampling every 100 trading periods.

Market parameters

The market will be simulated with \(N_a\) agents and a baseline model with the following parameters (as previously defined).

Parameter Value
\(N_a\) 1000
\(p_f\) \(1000\)
\(\Delta\) \(0.1\)
\(\tau\) \(50\)
\(\sigma_\epsilon\) \(0.01\)
\(\sigma_f\) Varies
\(\sigma_m\) Varies
\(\sigma_n\) \(1\)
\(k_{max}\) 0.5
\(L_{min}\) 5
\(L_{max}\) 50

Note: the parameters \(\sigma_f, \sigma_m, \tau\) are critical in the experiments and will be allowed to vary across different runs.

Python Code

This is the based python code for the core of the simulation runs. It also uses three objects which are loaded at the start, agent, orderBook, and forecasts. Links to all these files are given after the software listing.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# -*- coding: utf-8 -*-
"""
Created on Fri May 20 12:22:22 2016

@author: LeBaron
"""
# Author: Blake LeBaron
# This code is based on Chiarella/Iori, Quant Finance, 2002, vol 2, 346-353
# Main market simulation

# import usual Python helpers
import numpy as np
import numpy.random as rnd
from scipy.stats import kurtosis
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import scipy.stats as stats
import statsmodels.tsa.stattools as stattools

# import primary objects
from agent import agent
from forecasts import forecasts
from orderBook import orderBook

# helper routine for basic autocorrelations
def autocorr(x,m):
    n = len(x)
    v = x.var()
    x2 = x-x.mean()
    r = np.correlate(x2,x2,mode="full")[(n-1):(n+m+1)]
    result = r/(n*v)
    return result
    
def normhist(xdata,nbins):
    n, bins, patches = plt.hist(xdata,nbins,normed=1,facecolor='green', alpha=0.5)
    mu = np.mean(xdata)
    sigma = np.std(xdata)
    y = mlab.normpdf(bins,mu,sigma)
    plt.plot(bins,y,'r')
    
def reversion(xdata):
    xdiff = np.diff(xdata)
    rstat = np.corrcoef(xdata[0:-1],xdiff)
    return rstat[0,1]
    
def pltPrice(prices):
    plt.clf()
    plt.plot(prices)
    plt.xlabel('Period(t)')
    plt.ylabel('Price')
    plt.grid()
    
# set default parameters
nAgents = 1000
Tinit = 1000
Tmax = 50000
Lmin = 5
Lmax = 50
pf = 1000.
deltaP = 0.1
# deltaP = 0.5
sigmae = 0.01
kMax = 0.5
tau = 50
# tau = 10
runType = 0
if runType == 0:
    sigmaF = 0.
    sigmaM = 0.
    sigmaN = 1.
if runType == 1:
    sigmaF = 1.
    sigmaM = 0.
    sigmaN = 1.
if runType == 2:
    sigmaF = 1.
    sigmaM = 10.
    sigmaN = 1.

# time length for ticks per time period
deltaT = 100
# holder list for agent objects
agentList = []
# price, return, and volume time series
price = pf*np.ones(Tmax+1)
ret   = np.zeros(Tmax+1)
totalV = np.zeros(Tmax+1)
rprice = np.zeros((Tmax+1)/100)

# create agents in list of objects
for i in range(nAgents):
    agentList.append(agent(sigmaF,sigmaM,sigmaN,kMax,Lmin,Lmax))
# create set of 
forecastSet = forecasts(Lmax,pf,sigmae)
# create order book
marketBook = orderBook(600.,1400.,deltaP)
# set up initial prices

price[0:Tinit] = pf*(1.+0.001*np.random.randn(Tinit))
ret[0:Tinit] = 0.001*np.random.randn(Tinit)

for t in range(Tinit,Tmax):
    # update all forecasts
    forecastSet.updateForecasts(t,price[t],ret)
    tradePrice = -1
    # draw random agent
    randomAgent = agentList[np.random.randint(1,nAgents)]
    # set update current forecasts for random agent
    randomAgent.updateFcast(forecastSet,price[t],tau)
    # get demands for random agent
    randomAgent.getAgentOrder(price[t])
    # potential buyer
    if randomAgent.pfcast > price[t]:
        # add bid or market order 
        tradePrice = marketBook.addBid(randomAgent.bid,1.,t)
    else:
        # seller: add ask, or market order
        tradePrice = marketBook.addAsk(randomAgent.ask,1.,t)
    # update price and volume
    # no trade
    if tradePrice == -1:
        price[t+1]=(marketBook.bestBid + marketBook.bestAsk)/2.
        totalV[t+1]=totalV[t]
    else:
        # trade
        price[t+1] = tradePrice
        totalV[t+1] = totalV[t]+1.
    # returns
    ret[t+1]=np.log(price[t+1]/price[t])
    # clear book
    if(rnd.rand()<0.2):
        marketBook.cleanBook(t,tau)

# generate long run values for time series    
rVol    = np.diff(totalV[range(Tinit+deltaT,Tmax,deltaT)])
rPrice = price[ range(Tinit+deltaT,Tmax,deltaT)]
rret   = np.diff(np.log(rPrice))

plt.rcParams['lines.linewidth']=2
plt.rcParams['font.size']=14

print(np.var(rPrice))
print(np.var(rret))

simMarket

agent

orderBook

forecasts

Example runs

Noise only

The first run of the market consists of noise forecasts only. This is done by setting the parameters as follows, \(\sigma_f=\sigma_m=0\), and \(\sigma_n=1\). This experiment can be run from the python code with the variable runType set to 0. This gives traders whose forecasts are only based on noise. Also, the market has no sense of what the fundamental value is. Therefore, this is a low information benchmark, and we should expect very little structure in the price series it produces.

This is clear in figure figure1. This shows a price series which looks near random walk, and shows no tendency toward the fundamental value, 1000. These prices are sampled at a lower frequency than agent arrival occurs. Prices are sampled every 100 periods so in tick time they would be

\[P_t = S_{t*100}\]

where \(S_h\) is the price sampled at each trading tick.

_images/CInoise.png

Noise + fundamental

The next figure adds a weight on the fundamental forecasts. Traders are now augmented with \(\sigma_f=1,\sigma_m=0,\sigma_n=1\). This puts some weight on a return to the fundamental price of 1000. An example of these parameters can be run by setting runType to 1. Prices from this run are now compared with the previous case in figure Figure:Noise/Fundamental. The fundamental run is shown by the green line which displays a dramatically different outcome. The price no longer strays far from the fundamental value.

The strength of this mean reversion can be seen quantitatively in table Summary statistics which gives a few summary statistics on prices and returns in the different runs. These first two examples are shown in the first two columns. P(Std) represents the standard deviation of the price series itself. It makes sense to estimate this since most of the experiments here are stationary around 1000. It is clear that adding fundamental traders greatly reduces price variability in terms of this standard deviation, though this was already obvious in the graph.

The next rows of the table measure various statistics using the returns series defined as

\[R_t = \log(P_{t}) - \log(P_{t-1})\]

The first estimates the kurtosis on the returns. Excess kurtosis is one of our common stylized facts for returns. The plain noise case generates almost no excess kurtosis with an estimated value of 3.2 (3 is the value for a normal distribution). However this increases a little for the noise and fundamental case to 4.2.

First order return autocorrelations are estimated in the next row. These are often close to zero in actual data. For the noise case they are zero, but they jump to -0.5 for the noise+fundamental case. This shows that with fundamental beliefs the return series is strongly mean reverting.

The row labeled Reverse reports a statistic estimated as,

\[E( R_{t+1} P_t)\]

This is a crude estimate of the strength of mean reversion in the series. For a pure random walk this should be zero since the level of the price has no impact on the future change in returns. Mean reversion will drive this value negative which it clearly is (-0.70) for the noise and fundamental case, but nearly zero for the random walk case, (-0.06).

The final row in the table shows the first order autocorrelations for the absolute returns. In actual returns series this value is often significantly positive, and decays slowly. In the two cases we have looked at so far the value is very close to zero.

_images/CInoisefund.png

Figure:Noise/Fundamental

Summary statistics
Statistic Noise Noise+Fund Noise+Fund+Trend
P(Std) 200.6 26.6 104.4
R(Kurtosis) 3.2 4.2 4.5
R(ACF1) 0.00 -0.50 -0.07
Reverse -0.06 -0.70 -0.27
|R|(ACF1) 0.07 0.05 0.11
Volume 0.24 3.4 4.8
Volume(ACF1) -0.04 0.10 0.68

Noise + fundamental + trend

The next figure (figure3) adds a trend following weight to the model with the fundamental weight (red line). The parameters are given in \(\sigma_f=1,\sigma_m=10,\sigma_n=1\) This once again pushes the price variability away from the fundamental as in the noise trader only run. The presence of some weight on trend following beliefs now causes the price to take long persistent swings from the fundamental value as in the first case. However, this time forecasts give weight to deviations from fundamentals, but they are countered by the beliefs about persistent trends.

_images/CInoisefundtrend.png

Turning back to Summary statistics it repeats the basic results of the figure. This case generates results which are intermediate to the first two cases. Volatility and mean reversion measures are between the two other cases. For example, mean reversion as estimated by the first order autocorrelation, drops from -0.50 to -0.07, getting closer to the uncorrelated returns we see in actual data. It would appear that the trend following strategies do a good job of neutralizing some of the mean reversion from the fundamental strategies. Once again there is only a limited amount of excess kurtosis and persistence in absolute returns.

The final line in the table turns to the amount of trading volume in each of the cases. It is recorded in the vector rVol in the program. This is estimated as the mean number of shares traded over each 100 tick period. For the noise trader only case this is only about 0.24, indicating that on average, fewer than one share trades over each 100 period time block. This rises significantly to 3.4 and 4.8 for the fundamental and fundamental + trend cases respectively. It would appear that adding more heterogeneity to the forecast rules greatly increases the amount of trade. This may seem intuitive, but it is good to see it working its way through a limit order book.

The final row shows the first order autocorrelation for trading volume. The fact that this is large and positive in many series is another one of our big stylized facts. In this case, the autocorrelation is not very large, except for the case with trend following strategies. In that case it is 0.68 which is well over 6 times the value for the case without trend following. It appears that in this model trend following is necessary to generate persistent trading volume.

Changing trading specifications

Order cancellations

The simple structure of this model allows for some experiments in the rules used in the highly simplified trading mechanism. There are many ways the rules can be changed to see how they affect the eventual price dynamics.

The first key parameter we will change will be the order cancellation policy. Orders remain on the book for a fixed amount of time, given by the parameter \(\tau\). This has been fixed at 50 for the initial runs. We will now take the benchmark parameters with all forecasts active, and drop the time on the book to 10 ticks in trade time.

The impact of this change is dramatic. The next figure plots the simulated price series. As before it is plotted at increments of 100 trading ticks. Price reversals are now huge and the market swings dramatically between the price limits. This is related to the relative emptiness of the limit order book in this case. Trades now sweep across large empty spaces in the book leading to increased volatility. These big jumps are probably enhanced by the presence of trend following strategies.

_images/CItau10.png

The next figure repeats the plot for the return series.

_images/CItau10ret.png

This table shows much of what is visible in the plots. Clearing out the book has increased price volatility, and the strength of mean reversion. This latter fact is seen in the large negative autocorrelation in the returns series, and the reversal statistic of -0.67. Somewhat surprisingly it has not impacted return kurtosis, or the weak autocorrelations in the absolute returns. Even more surprising is the feature that trading volume actually increases.

Summary statistics
Statistic Tau=10 Delta P = 0.5
P(Std) 219.7 69.4
R(Kurtosis) 2.6 7.26
R(ACF1) -0.41 -0.05
Reverse -0.67 -0.34
|R|(ACF1) 0.04 0.01
Volume 5.99 2.7
Volume(ACF1) 0.23 0.48

Price discreteness

Another parameter that is part of the microstructure of the market institutions is the price discreteness. All trading occurs on a discrete grid of prices. For all experiments performed so far the price range is [600,1400] with the price changes fixed at 0.1. This yields a very rich grid for trading and order placement. Changing the price discreteness is a very interesting policy question. For U.S. equity markets trading used to occur at 1/8 of a dollar, but was eventually changed to pennies. The impact of this change on markets is still an interesting question.

This next experiment increases the deltaP parameter controlling price discreteness to 0.5. The following figure shows the price dynamics in this case. The price can still take some long moves, but volatility around the fundamental has decreased.

Column 3 in the previous table shows that these changes contrast quantitatively with the benchmark case in the first table. The price standard deviation falls, and the reversal statistic indicates a stronger reversion toward the fundamental. A major surprising result is the increase in return kurtosis. This increase in discreteness has reduced some of the price continuity. Trading volume falls as traders have a harder time matching with others. Finally, the market again shows very little persistence in absolute returns.

_images/CIdeltaP05.png

Summary

The limit order model of [CI02] has provided a simplified picture of basic market microstructure. Using relatively realistic trading institutions it adds a small amount of predictive intelligence to basic near zero intelligence trading strategies.

The model is capable of generating some realistic features of financial markets that are also closely related to some of the other agent-based markets that we have looked at in early lectures. The combination of mean reversion and trend following strategies can lead to relatively uncorrelated returns in a market where prices may still take long swings from fundamental values. Trading activity, in terms of trading volume, appears to be much larger, and more persistent than that of a simple homogenous noise trader world.

One interesting feature of these markets is that they generally show little indication for the persistence of volatility which is a major aspect of real market data. Also, at the horizons where we are measuring returns, evidence for excess kurtosis seems relatively weak. It is possible that other mechanisms (order splitting, learning) are necessary to generate these features.

Finally, the model allows for changes in the mechanics of trading. Many experiments would be possible here, but only two major ones were performed. First, increasing the speed of order cancelation increases price variability and mean reversion. The price series is actually unrealistically thrashing back and forth between the extreme price levels. Surprisingly, this change also increases trading volume. Second, increasing the discreteness of price changes helps to stabilize the price around the fundamental. Price variability falls, and there are less strong excursions from the fundamental. Trading volume also falls as the discreteness in the orderbook is increased.

Some of these interesting nonmonotonic relationships are similar to those found in [CI02], and we will explore these further in other lectures.