Another implementation of stock matching engine which uses binary heaps to store prices in bid/ask order books, and balances the orders recursively.
******************************* Exchange ********************************
The value of 'order_id' reflects the order of arrival to the exchange. Orders are executed in the priority of the 'order_id'. Orders with a smaller 'order_id', other conditions being equal, get to be executed first, because these are the orders which arrived earlier.
Both of binary heaps, 'buys_prices' and 'sells_prices', are min-heaps. 'buys_prices' heap contains minus of submitted buy prices. The binary heaps here contain unique entries. When we store information about new orders at each price level in the 'buys_info' and 'sells_info' we check first whether that price is already there. We can use that check to also see whether we need to push that price into the corresponding heap, and avoid duplicates.
Exchange class attributes:
Binary heaps with unique (negative) limit prices for (buys) sells:
buys_prices (list)
sells_prices (list)
Order ID and size information for orders at given prices:
buys_info (dict)
sells_info (dict)
An entry in the dictionary contains the list of order IDs
in the order of arrival, dictionary of sizes of the orders
(map between order IDs and order sizes), and the total
current volume of orders at the given price level.
Bid and ask prices time series:
bids (list)
asks (list)
Self-incremented order ID:
order_id
Instantaneous stored information on filled orders:
buys_filled (dict)
sells_filled (dict)
Exchange class methods:
start_price. Seed price. An artifact of the specific model example
considered here, used for the consitent recording of the bid and ask
prices time series. Can be altered for a different simulation purposes,
but make sure to take it into account in the savePrices, which might be
pathological if the buys and/or the sells have been completely filled
so that now best buys and/or best sells could be added to the time series.
In that case we need to add the price from the previous time step,
which in the most straightforward implementation requires an initial
seed price to start with.
reset. Reset the order_id count to -1.
addOrder. Arguments: side of the order ('B' or 'S'), limit price, size.
Increments the order_id count by one and assigns it to the current
order. Returns the order_id.
When the new order arrives, check whether the price level (as
defined by the limit price) for that order already exists in the
heap. Since we will need to prepare an entry for the order info
dictionary, we combine these two checks, by checking whether the
order info dictionary contains that limit price among its keys.
prepareMatch. Ancillary method for the matchOrders method (see below).
Clears up buys_filled and sells_filled dictionaries of the orders,
which will then be populated during the process of the matchOrders
filling out the orders.
matchOrders. Recursive filling of all the orders. Calls itself until
the buy and the sell books are balanced. Doesn't return any output,
but acts on the class attributes buys_filled, sells_filled, storing
there the orderID:(size,price) for the orders filled during its
execution. Here is how it works:
1. Check whether the current best prices in the buy and sell
books actually have zero volume. This might be a leftover of
the orders that already got executed. If true, clean those price
levels from the heaps and from the order info books.
2. Check whether the books are balanced. This will happen if
either any of the books is empty, or the best buy is lower
than the best sell.
3. Update the best buy and the best sell prices, because we
might have removed the previous best ones from the heaps, if the
corresponding orders have already been filled.
4. Take the earliest orders from the best buy and the best sell
orders. Record their order IDs and sizes.
5. Fill the smaller sized order in 'filled'.
6. Record the executed orders in the buys_filled and sells_filled.
The orders will be executed at the price which is the mean of the
submitted limit price and the best limit price from the opposite
order book. Therefore each order might get executed in different
portions and at different sizes (we assume that no orders are of
the 'all or none' condition). Record the prices and sizes tuples
each time the order gets executed.
7. Removed the 'filled' volume from the total volumes of the buy
and sell books.
8. Subtract the filled size from the orders volumes.
9. Clean up the orders which have been totally filled.
10. Recurrently call itself.
savePrices. Save the current best prices in the buy and the sell books.
To be called after the matchOrders.
******************************* Test1 *********************************
Add random orders to the exchange and try to fill the orders (match with the orders on the opposite side). Plot the balanced buy and sell order books, and the bid and ask prices time series.
******************************* Test2 *********************************
Simulate a random stock market activity over several trading session. Each trading session is defined by a new exchange, and therefore unfilled orders from the previous trading sessions are emptied (this restriction can be relaxed). At each trading session each agent participates in the trade with the probability 10%
Each Agent is originally endowed with (almost) the same number of shares of stock and amount of cash; up to small normal variability. The agents trade randomly, submitting buy/sell orders with 50/50% probability, and choose the size of the trade according to a uniform distribution (Genoa Stock Market, see https://arxiv.org/pdf/cond-mat/0103600.pdf and references therein). The agents choose limit price as a gaussian variable centered around the last price.
Plot the resulting stock price time series (stock price is chosen as the mean between the best bid and the best ask), the QQ plot of the stock returns, and the final wealth distribution. Notice the Pareto tail of the final wealth distribution. Stock price exhibits a mean-reversing beahavior around the equilibrium price (which under the current conditions is equal to the total amount of cash divided by the total amount of shares), which is approximately equal to the starting price, as specified here. Stock returns are distributed normally, due to the passive trading of the agents.