Unit testing a simple limit order book with Python

This post is a follow-up to the previous post on building a simple limit order book with Python. After that original post, I learned that there were some quirks in how the WordPress editor handles the “less than” symbol in code blocks. That post has been updated and I will continue to monitor it for any more corruption.

I will walk through unit testing the orderbook methods with the unittest module. There are a variety of python testing alternatives. I chose unittest for two reasons: 1.) unittest is included in the standard library; and 2.) unittest works well with the Eclipse/PyDev Integrated Development Environment (IDE). Many Python aristocrats (Pythonistocrats?) have adopted Pytest because it is easier to implement with automated build processes. See for example the pandas documentation for a discussion of how they incorporate Pytest into their continuous integration services. I find the pandas documentation a very helpful resource for learning how to manage the test-build-ship process from GitHub.

Nearly every post on code testing will admonish you to implement test-driven development. But I admit that for me the combination of the orderbook and the tests is more like test-enhanced development: I would write the basics of the method, then test, and then repeat if necessary. To satisfy the rule on writing about testing, I recommend you use test-driven development!

Testing begins with creating a separate module, importing the module/class to be tested and unittest, and defining a test class that inherits from unittest.TestCase. The full code and directory structure is available in my GitHub repo. You might find it helpful to have the actual Orderbook code handy when walking through the tests.

from pyziabm.orderbook3 import Orderbook
import unittest

class TestOrderbook(unittest.TestCase):

There is a special method in unittest called setUp(). This method is called every time a test method is called. We will use it to provide a clean Orderbook instance and a set of known orders to each test.

    def setUp(self):
        '''
        setUp creates the Orderbook instance and a set of orders
        '''
        self.ex1 = Orderbook()
        self.q1_buy = {'order_id': 't1_1', 'timestamp': 2, 'type': 'add', 'quantity': 1, 'side': 'buy',
                       'price': 50}
        self.q2_buy = {'order_id': 't1_2', 'timestamp': 3, 'type': 'add', 'quantity': 1, 'side': 'buy',
                       'price': 50}
        self.q3_buy = {'order_id': 't10_1', 'timestamp': 4, 'type': 'add', 'quantity': 3, 'side': 'buy',
                       'price': 49}
        self.q4_buy = {'order_id': 't11_1', 'timestamp': 5, 'type': 'add', 'quantity': 3, 'side': 'buy',
                       'price': 47}
        self.q1_sell = {'order_id': 't1_3', 'timestamp': 2, 'type': 'add', 'quantity': 1, 'side': 'sell',
                        'price': 52}
        self.q2_sell = {'order_id': 't1_4', 'timestamp': 3, 'type': 'add', 'quantity': 1, 'side': 'sell',
                        'price': 52}
        self.q3_sell = {'order_id': 't10_2', 'timestamp': 4, 'type': 'add', 'quantity': 3, 'side': 'sell',
                        'price': 53}
        self.q4_sell = {'order_id': 't11_2', 'timestamp': 5, 'type': 'add', 'quantity': 3, 'side': 'sell',
                        'price': 55}

The testing strategy for the Orderbook instance (self.ex1) is similar for all of the tests: establish the state of the orderbook before calling the orderbook method (if necessary), create any needed inputs, call the method, and finally, test that the output matches what is expected. All test methods must begin with the word ‘test’. test_add_order_to_history() is a simple example of the strategy.

    def test_add_order_to_history(self):
        '''
        add_order_to_history() impacts the order_history list
        '''
        h1 = {'order_id': 't1_5', 'timestamp': 4, 'type': 'add', 'quantity': 5, 'side': 'sell', 'price': 55}
        self.assertFalse(self.ex1.order_history)
        h1['exid'] = 1
        self.ex1._add_order_to_history(h1)
        self.assertDictEqual(h1, self.ex1.order_history[0])

In this test, h1 is the input dict. The next line asserts that the order history list is empty. After appending the exchange order id to the dict, we call the method with h1. Finally, we assert that the modified h1 dict from the test method matches the first (and only) dict in the exchange order history list. If the assertions pass (return True), then the test will pass. In PyDev, the tests are run from a menu. In the console area, it will return something like:

Finding files... done.
Importing test modules ... done.
----------------------------------------------------------------------
Ran 1 tests in 0.000s

OK

You can also run the test module from a shell: python –m unittest testOrderbook3.

test_add_order_to_book() follows a similar strategy. First we check that the price list and the book dict are both empty (because setUp() was just called). Then we add one order to the bid book with self.ex1.add_order_to_book(self.q1_buy) and test whether actual and expected are the same with simple assertions: assertTrue, assertEqual, asserDictEqual. We then add another order to check whether the incrementing portion of add_order_to_book is working correctly. Finally, we repeat the process for sell orders.

    def test_add_order_to_book(self):
        '''
        add_order_to_book() impacts _bid_book and _bid_book_prices or _ask_book and _ask_book_prices
        Add two buy orders, then two sell orders
        '''
        # 2 buy orders
        self.assertFalse(self.ex1._bid_book_prices)
        self.assertFalse(self.ex1._bid_book)
        self.ex1.add_order_to_book(self.q1_buy)
        self.assertTrue(50 in self.ex1._bid_book_prices)
        self.assertTrue(50 in self.ex1._bid_book.keys())
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 1)
        self.assertEqual(self.ex1._bid_book[50]['size'], 1)
        self.assertEqual(self.ex1._bid_book[50]['order_ids'][0], self.q1_buy['order_id'])
        self.assertDictEqual(self.ex1._bid_book[50]['orders'][self.q1_buy['order_id']], self.q1_buy)
        self.ex1.add_order_to_book(self.q2_buy)
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 2)
        self.assertEqual(self.ex1._bid_book[50]['size'], 2)
        self.assertEqual(self.ex1._bid_book[50]['order_ids'][1], self.q2_buy['order_id'])
        self.assertDictEqual(self.ex1._bid_book[50]['orders'][self.q2_buy['order_id']], self.q2_buy)
        # 2 sell orders
        self.assertFalse(self.ex1._ask_book_prices)
        self.assertFalse(self.ex1._ask_book)
        self.ex1.add_order_to_book(self.q1_sell)
        self.assertTrue(52 in self.ex1._ask_book_prices)
        self.assertTrue(52 in self.ex1._ask_book.keys())
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 1)
        self.assertEqual(self.ex1._ask_book[52]['size'], 1)
        self.assertEqual(self.ex1._ask_book[52]['order_ids'][0], self.q1_sell['order_id'])
        self.assertDictEqual(self.ex1._ask_book[52]['orders'][self.q1_sell['order_id']], self.q1_sell)
        self.ex1.add_order_to_book(self.q2_sell)
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 2)
        self.assertEqual(self.ex1._ask_book[52]['size'], 2)
        self.assertEqual(self.ex1._ask_book[52]['order_ids'][1], self.q2_sell['order_id'])
        self.assertDictEqual(self.ex1._ask_book[52]['orders'][self.q2_sell['order_id']], self.q2_sell)

test_remove_order() first adds two orders and checks the state of the book. Then it removes the two orders and checks the orderbook state after each removal. Finally, it also checks that removing an order that is not there causes no harm. The process is then repeated for sell orders.

    def test_remove_order(self):
        '''
        _remove_order() impacts _bid_book and _bid_book_prices or _ask_book and _ask_book_prices
        Add two  orders, remove the second order twice
        '''
        # buy orders
        self.ex1.add_order_to_book(self.q1_buy)
        self.ex1.add_order_to_book(self.q2_buy)
        self.assertTrue(50 in self.ex1._bid_book_prices)
        self.assertTrue(50 in self.ex1._bid_book.keys())
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 2)
        self.assertEqual(self.ex1._bid_book[50]['size'], 2)
        self.assertEqual(len(self.ex1._bid_book[50]['order_ids']), 2)
        # remove first order
        self.ex1._remove_order('buy', 50, 't1_1')
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 1)
        self.assertEqual(self.ex1._bid_book[50]['size'], 1)
        self.assertEqual(len(self.ex1._bid_book[50]['order_ids']), 1)
        self.assertFalse('t1_1' in self.ex1._bid_book[50]['orders'].keys())
        self.assertTrue(50 in self.ex1._bid_book_prices)
        # remove second order
        self.ex1._remove_order('buy', 50, 't1_2')
        self.assertFalse(self.ex1._bid_book_prices)
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 0)
        self.assertEqual(self.ex1._bid_book[50]['size'], 0)
        self.assertEqual(len(self.ex1._bid_book[50]['order_ids']), 0)
        self.assertFalse('t1_2' in self.ex1._bid_book[50]['orders'].keys())
        self.assertFalse(50 in self.ex1._bid_book_prices)
        # remove second order again
        self.ex1._remove_order('buy', 50, 't1_2')
        self.assertFalse(self.ex1._bid_book_prices)
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 0)
        self.assertEqual(self.ex1._bid_book[50]['size'], 0)
        self.assertEqual(len(self.ex1._bid_book[50]['order_ids']), 0)
        self.assertFalse('t1_2' in self.ex1._bid_book[50]['orders'].keys())
        # sell orders
        self.ex1.add_order_to_book(self.q1_sell)
        self.ex1.add_order_to_book(self.q2_sell)
        self.assertTrue(52 in self.ex1._ask_book_prices)
        self.assertTrue(52 in self.ex1._ask_book.keys())
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 2)
        self.assertEqual(self.ex1._ask_book[52]['size'], 2)
        self.assertEqual(len(self.ex1._ask_book[52]['order_ids']), 2)
        # remove first order
        self.ex1._remove_order('sell', 52, 't1_3')
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 1)
        self.assertEqual(self.ex1._ask_book[52]['size'], 1)
        self.assertEqual(len(self.ex1._ask_book[52]['order_ids']), 1)
        self.assertFalse('t1_1' in self.ex1._ask_book[52]['orders'].keys())
        self.assertTrue(52 in self.ex1._ask_book_prices)
        # remove second order
        self.ex1._remove_order('sell', 52, 't1_4')
        self.assertFalse(self.ex1._ask_book_prices)
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 0)
        self.assertEqual(self.ex1._ask_book[52]['size'], 0)
        self.assertEqual(len(self.ex1._ask_book[52]['order_ids']), 0)
        self.assertFalse('t1_2' in self.ex1._ask_book[52]['orders'].keys())
        self.assertFalse(52 in self.ex1._ask_book_prices)
        # remove second order again
        self.ex1._remove_order('sell', 52, 't1_4')
        self.assertFalse(self.ex1._ask_book_prices)
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 0)
        self.assertEqual(self.ex1._ask_book[52]['size'], 0)
        self.assertEqual(len(self.ex1._ask_book[52]['order_ids']), 0)
        self.assertFalse('t1_2' in self.ex1._ask_book[52]['orders'].keys())

In modern limit order book markets, some order modifications do not generally result in loss of time priority. Reducing the limit order quantity is one example of this type of modification. test_modify_order() begins by adding an order with quantity of 2 to a clean orderbook and then tests for a reduction in quantity and finally tests for removal when quantity becomes zero. The tests are repeated for sell orders.

    def test_modify_order(self):
        '''
        _modify_order() primarily impacts _bid_book or _ask_book
        _modify_order() could impact _bid_book_prices or _ask_book_prices if the order results
        in removing the full quantity with a call to _remove_order()
        Add 1 order, remove partial, then remainder
        '''
        # Buy order
        q1 = {'order_id': 't1_1', 'timestamp': 5, 'type': 'add', 'quantity': 2, 'side': 'buy',
              'price': 50}
        self.ex1.add_order_to_book(q1)
        self.assertEqual(self.ex1._bid_book[50]['size'], 2)
        # remove 1
        self.ex1._modify_order('buy', 1, 't1_1', 50)
        self.assertEqual(self.ex1._bid_book[50]['size'], 1)
        self.assertEqual(self.ex1._bid_book[50]['orders']['t1_1']['quantity'], 1)
        self.assertTrue(self.ex1._bid_book_prices)
        # remove remainder
        self.ex1._modify_order('buy', 1, 't1_1', 50)
        self.assertFalse(self.ex1._bid_book_prices)
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 0)
        self.assertEqual(self.ex1._bid_book[50]['size'], 0)
        self.assertFalse('t1_1' in self.ex1._bid_book[50]['orders'].keys())
        # Sell order
        q2 = {'order_id': 't1_1', 'timestamp': 5, 'type': 'add', 'quantity': 2, 'side': 'sell',
              'price': 50}
        self.ex1.add_order_to_book(q2)
        self.assertEqual(self.ex1._ask_book[50]['size'], 2)
        # remove 1
        self.ex1._modify_order('sell', 1, 't1_1', 50)
        self.assertEqual(self.ex1._ask_book[50]['size'], 1)
        self.assertEqual(self.ex1._ask_book[50]['orders']['t1_1']['quantity'], 1)
        self.assertTrue(self.ex1._ask_book_prices)
        # remove remainder
        self.ex1._modify_order('sell', 1, 't1_1', 50)
        self.assertFalse(self.ex1._ask_book_prices)
        self.assertEqual(self.ex1._ask_book[50]['num_orders'], 0)
        self.assertEqual(self.ex1._ask_book[50]['size'], 0)
        self.assertFalse('t1_1' in self.ex1._ask_book[50]['orders'].keys())

test_add_trade_to_book(), test_confirm_trade() and test_confirm_modify() all test whether a python dict has been added to a list. These tests follow the same strategy as test_add_order_to_history().

    def test_add_trade_to_book(self):
        '''
        add_trade_to_book() impacts trade_book
        Check trade book empty, add a trade, check non-empty, verify dict equality
        '''
        t1 = dict(resting_order_id='t1_1', resting_timestamp=2, incoming_order_id='t2_1',
                  timestamp=5, price=50, quantity=1, side='buy')
        self.assertFalse(self.ex1.trade_book)
        self.ex1._add_trade_to_book('t1_1', 2, 't2_1', 5, 50, 1, 'buy')
        self.assertTrue(self.ex1.trade_book)
        self.assertDictEqual(t1, self.ex1.trade_book[0])

    def test_confirm_trade(self):
        '''
        confirm_trade() impacts confirm_trade_collector
        Check confirm trade collector empty, add a trade, check non-empty, verify dict equality
        '''
        t2 = dict(timestamp=5, trader='t3', order_id='t3_1', quantity=1,
                  side='sell', price=50)
        self.assertFalse(self.ex1.confirm_trade_collector)
        self.ex1._confirm_trade(5, 'sell', 1, 't3_1', 50)
        self.assertTrue(self.ex1.confirm_trade_collector)
        self.assertDictEqual(t2, self.ex1.confirm_trade_collector[0])

    def test_confirm_modify(self):
        '''
        confirm_modify() impacts confirm_modify_collector
        Check confirm modify collector empty, add a trade, check non-empty, verify dict equality
        '''
        m1 = dict(timestamp=7, trader='t5', order_id='t5_10', quantity=5, side='buy')
        self.assertFalse(self.ex1.confirm_modify_collector)
        self.ex1._confirm_modify(7, 'buy', 5, 't5_10')
        self.assertTrue(self.ex1.confirm_modify_collector)
        self.assertDictEqual(m1, self.ex1.confirm_modify_collector[0])

In the Orderbook instance, process_order() potentially relies upon _match_trade(). Testing these independently is difficult. I decided to test process_order() with a simple trade quantity of 1 and then test for proper matching (i.e., “walking the book”) with quantities > 1 in _match_trade(). test_process_order() seeds each side of the orderbook with 2 orders with the same price, then tests the impact of marketable buy and sell orders with quantity 1. It then tests for adding, canceling and modifying some orders. See the docstring and inline comments for more details.

    def test_process_order(self):
        '''
        process_order() impacts confirm_modify_collector, traded indicator, order_history,
        _bid_book and _bid_book_prices or _ask_book and _ask_book_prices.
        process_order() is a traffic manager. An order is either an add order or not. If it is an add order,
        it is either priced to go directly to the book or is sent to match_trade (which is tested below). If it
        is not an add order, it is either modified or cancelled. To test, we will add some buy and sell orders,
        then test for trades, cancels and modifies. process_order() also resets some object collectors.
        '''
        self.q2_buy['quantity'] = 2
        self.q2_sell['quantity'] = 2

        self.assertEqual(len(self.ex1._ask_book_prices), 0)
        self.assertEqual(len(self.ex1._bid_book_prices), 0)
        self.assertFalse(self.ex1.confirm_modify_collector)
        self.assertFalse(self.ex1.order_history)
        self.assertFalse(self.ex1.traded)
        # seed order book
        self.ex1.add_order_to_book(self.q1_buy)
        self.ex1.add_order_to_book(self.q1_sell)
        # process new orders
        self.ex1.process_order(self.q2_buy)
        self.ex1.process_order(self.q2_sell)
        self.assertEqual(len(self.ex1._ask_book_prices), 1)
        self.assertEqual(len(self.ex1._bid_book_prices), 1)
        self.assertEqual(len(self.ex1.order_history), 2)
        # marketable sell takes out 1 share
        q3_sell = {'order_id': 't3_1', 'timestamp': 5, 'type': 'add', 'quantity': 1, 'side': 'sell',
                   'price': 0}
        self.ex1.process_order(q3_sell)
        self.assertEqual(len(self.ex1.order_history), 3)
        self.assertEqual(self.ex1._bid_book[50]['num_orders'], 1)
        self.assertEqual(self.ex1._bid_book[50]['size'], 2)
        self.assertTrue(self.ex1.traded)
        # marketable buy takes out 1 share
        q3_buy = {'order_id': 't3_2', 'timestamp': 5, 'type': 'add', 'quantity': 1, 'side': 'buy',
                  'price': 10000}
        self.ex1.process_order(q3_buy)
        self.assertEqual(len(self.ex1.order_history), 4)
        self.assertEqual(self.ex1._ask_book[52]['num_orders'], 1)
        self.assertEqual(self.ex1._ask_book[52]['size'], 2)
        self.assertTrue(self.ex1.traded)
        # add/cancel buy order
        q4_buy = {'order_id': 't4_1', 'timestamp': 10, 'type': 'add', 'quantity': 1, 'side': 'buy',
                  'price': 48}
        self.ex1.process_order(q4_buy)
        self.assertEqual(len(self.ex1.order_history), 5)
        self.assertEqual(len(self.ex1._bid_book_prices), 2)
        self.assertEqual(self.ex1._bid_book[48]['num_orders'], 1)
        self.assertEqual(self.ex1._bid_book[48]['size'], 1)
        self.assertFalse(self.ex1.traded)
        q4_cancel1 = {'order_id': 't4_1', 'timestamp': 10, 'type': 'cancel', 'quantity': 1, 'side': 'buy',
                     'price': 48}
        self.ex1.process_order(q4_cancel1)
        self.assertEqual(len(self.ex1.order_history), 6)
        self.assertEqual(len(self.ex1._bid_book_prices), 1)
        self.assertFalse(self.ex1.traded)
        # add/cancel sell order
        q4_sell = {'order_id': 't4_2', 'timestamp': 10, 'type': 'add', 'quantity': 1, 'side': 'sell',
                  'price': 54}
        self.ex1.process_order(q4_sell)
        self.assertEqual(len(self.ex1.order_history), 7)
        self.assertEqual(len(self.ex1._ask_book_prices), 2)
        self.assertEqual(self.ex1._ask_book[54]['num_orders'], 1)
        self.assertEqual(self.ex1._ask_book[54]['size'], 1)
        self.assertFalse(self.ex1.traded)
        q4_cancel2 = {'order_id': 't4_2', 'timestamp': 10, 'type': 'cancel', 'quantity': 1, 'side': 'sell',
                     'price': 54}
        self.ex1.process_order(q4_cancel2)
        self.assertEqual(len(self.ex1.order_history), 8)
        self.assertEqual(len(self.ex1._ask_book_prices), 1)
        self.assertFalse(self.ex1.traded)
        # add/modify buy order
        q5_buy = {'order_id': 't5_1', 'timestamp': 10, 'type': 'add', 'quantity': 5, 'side': 'buy',
                  'price': 48}
        self.ex1.process_order(q5_buy)
        self.assertEqual(len(self.ex1.order_history), 9)
        self.assertEqual(len(self.ex1._bid_book_prices), 2)
        self.assertEqual(self.ex1._bid_book[48]['num_orders'], 1)
        self.assertEqual(self.ex1._bid_book[48]['size'], 5)
        q5_modify1 = {'order_id': 't5_1', 'timestamp': 10, 'type': 'modify', 'quantity': 2, 'side': 'buy',
                     'price': 48}
        self.ex1.process_order(q5_modify1)
        self.assertEqual(len(self.ex1.order_history), 10)
        self.assertEqual(len(self.ex1._bid_book_prices), 2)
        self.assertEqual(self.ex1._bid_book[48]['size'], 3)
        self.assertEqual(self.ex1._bid_book[48]['orders']['t5_1']['quantity'], 3)
        self.assertEqual(len(self.ex1.confirm_modify_collector), 1)
        self.assertFalse(self.ex1.traded)
        # add/modify sell order
        q5_sell = {'order_id': 't5_1', 'timestamp': 10, 'type': 'add', 'quantity': 5, 'side': 'sell',
                  'price': 54}
        self.ex1.process_order(q5_sell)
        self.assertEqual(len(self.ex1.order_history), 11)
        self.assertEqual(len(self.ex1._ask_book_prices), 2)
        self.assertEqual(self.ex1._ask_book[54]['num_orders'], 1)
        self.assertEqual(self.ex1._ask_book[54]['size'], 5)
        q5_modify2 = {'order_id': 't5_1', 'timestamp': 10, 'type': 'modify', 'quantity': 2, 'side': 'sell',
                     'price': 54}
        self.ex1.process_order(q5_modify2)
        self.assertEqual(len(self.ex1.order_history), 12)
        self.assertEqual(len(self.ex1._ask_book_prices), 2)
        self.assertEqual(self.ex1._ask_book[54]['size'], 3)
        self.assertEqual(self.ex1._ask_book[54]['orders']['t5_1']['quantity'], 3)
        self.assertEqual(len(self.ex1.confirm_modify_collector), 1)
        self.assertFalse(self.ex1.traded)

For _match_trade(), we will test buys and sells separately. The logic is the same and is documented in the comments. In general, the tests check for partial executions (one incoming order with a quantity less than the quantity available at that price), walking the book (incoming order priced to remove more than one order from the book – possibly at different prices), and making a new market. First some sell orders:

    def test_match_trade_sell(self):
        '''
        An incoming order can:
        1. take out part of an order,
        2. take out an entire price level,
        3. if priced, take out a price level and make a new inside market.
        '''
        # seed order book
        self.ex1.add_order_to_book(self.q1_buy)
        self.ex1.add_order_to_book(self.q1_sell)
        # process new orders
        self.ex1.process_order(self.q2_buy)
        self.ex1.process_order(self.q2_sell)
        self.ex1.process_order(self.q3_buy)
        self.ex1.process_order(self.q3_sell)
        self.ex1.process_order(self.q4_buy)
        self.ex1.process_order(self.q4_sell)
        # The book: bids: 2@50, 3@49, 3@47 ; asks: 2@52, 3@53, 3@55
        self.assertEqual(self.ex1._bid_book[47]['size'], 3)
        self.assertEqual(self.ex1._bid_book[49]['size'], 3)
        self.assertEqual(self.ex1._bid_book[50]['size'], 2)
        self.assertEqual(self.ex1._ask_book[52]['size'], 2)
        self.assertEqual(self.ex1._ask_book[53]['size'], 3)
        self.assertEqual(self.ex1._ask_book[55]['size'], 3)
        #self.assertFalse(self.ex1.sip_collector)
        # market sell order takes out part of first best bid
        q1 = {'order_id': 't100_1', 'timestamp': 10, 'type': 'add', 'quantity': 1, 'side': 'sell',
              'price': 0}
        self.ex1.process_order(q1)
        self.assertEqual(self.ex1._bid_book[50]['size'], 1)
        self.assertTrue(50 in self.ex1._bid_book_prices)
        self.assertEqual(self.ex1._bid_book[49]['size'], 3)
        self.assertEqual(self.ex1._bid_book[47]['size'], 3)
        self.assertEqual(self.ex1._bid_book[50]['orders'][self.ex1._bid_book[50]['order_ids'][0]]['quantity'], 1)
        #self.assertEqual(len(self.ex1.sip_collector), 1)
        # market sell order takes out remainder first best bid and all of the next level
        self.assertEqual(len(self.ex1._bid_book_prices), 3)
        q2 = {'order_id': 't100_2', 'timestamp': 11, 'type': 'add', 'quantity': 4, 'side': 'sell',
              'price': 0}
        self.ex1.process_order(q2)
        self.assertEqual(len(self.ex1._bid_book_prices), 1)
        self.assertFalse(50 in self.ex1._bid_book_prices)
        self.assertFalse(49 in self.ex1._bid_book_prices)
        self.assertTrue(47 in self.ex1._bid_book_prices)
        #self.assertEqual(len(self.ex1.sip_collector), 3)
        # make new market
        q3 = {'order_id': 't101_1', 'timestamp': 12, 'type': 'add', 'quantity': 2, 'side': 'buy',
              'price': 48}
        q4 = {'order_id': 't102_1', 'timestamp': 13, 'type': 'add', 'quantity': 3, 'side': 'sell',
              'price': 48}
        self.ex1.process_order(q3)
        self.assertEqual(len(self.ex1._bid_book_prices), 2)
        self.assertTrue(48 in self.ex1._bid_book_prices)
        self.assertTrue(47 in self.ex1._bid_book_prices)
        self.assertEqual(self.ex1._bid_book_prices[-1], 48)
        self.assertEqual(self.ex1._bid_book_prices[-2], 47)
        # sip_collector does not reset until new trade at new time
        #self.assertEqual(len(self.ex1.sip_collector), 3)
        self.ex1.process_order(q4)
        self.assertEqual(len(self.ex1._bid_book_prices), 1)
        self.assertFalse(48 in self.ex1._bid_book_prices)
        self.assertTrue(47 in self.ex1._bid_book_prices)
        self.assertEqual(len(self.ex1._ask_book_prices), 4)
        self.assertTrue(48 in self.ex1._ask_book_prices)
        self.assertEqual(self.ex1._ask_book_prices[0], 48)
        self.assertEqual(self.ex1._bid_book_prices[-1], 47)
        #self.assertEqual(len(self.ex1.sip_collector), 1)

Then some buy orders:

    def test_match_trade_buy(self):
        '''
        An incoming order can:
        1. take out part of an order,
        2. take out an entire price level,
        3. if priced, take out a price level and make a new inside market.
        '''
        # seed order book
        self.ex1.add_order_to_book(self.q1_buy)
        self.ex1.add_order_to_book(self.q1_sell)
        # process new orders
        self.ex1.process_order(self.q2_buy)
        self.ex1.process_order(self.q2_sell)
        self.ex1.process_order(self.q3_buy)
        self.ex1.process_order(self.q3_sell)
        self.ex1.process_order(self.q4_buy)
        self.ex1.process_order(self.q4_sell)
        # The book: bids: 2@50, 3@49, 3@47 ; asks: 2@52, 3@53, 3@55
        self.assertEqual(self.ex1._bid_book[47]['size'], 3)
        self.assertEqual(self.ex1._bid_book[49]['size'], 3)
        self.assertEqual(self.ex1._bid_book[50]['size'], 2)
        self.assertEqual(self.ex1._ask_book[52]['size'], 2)
        self.assertEqual(self.ex1._ask_book[53]['size'], 3)
        self.assertEqual(self.ex1._ask_book[55]['size'], 3)
        # market buy order takes out part of first best ask
        q1 = {'order_id': 't100_1', 'timestamp': 10, 'type': 'add', 'quantity': 1, 'side': 'buy',
              'price': 100000}
        self.ex1.process_order(q1)
        self.assertEqual(self.ex1._ask_book[52]['size'], 1)
        self.assertTrue(52 in self.ex1._ask_book_prices)
        self.assertEqual(self.ex1._ask_book[53]['size'], 3)
        self.assertEqual(self.ex1._ask_book[55]['size'], 3)
        self.assertEqual(self.ex1._ask_book[52]['orders'][self.ex1._ask_book[52]['order_ids'][0]]['quantity'], 1)
        # market buy order takes out remainder first best ask and all of the next level
        self.assertEqual(len(self.ex1._ask_book_prices), 3)
        q2 = {'order_id': 't100_2', 'timestamp': 11, 'type': 'add', 'quantity': 4, 'side': 'buy',
              'price': 100000}
        self.ex1.process_order(q2)
        self.assertEqual(len(self.ex1._ask_book_prices), 1)
        self.assertFalse(52 in self.ex1._ask_book_prices)
        self.assertFalse(53 in self.ex1._ask_book_prices)
        self.assertTrue(55 in self.ex1._ask_book_prices)
        # make new market
        q3 = {'order_id': 't101_1', 'timestamp': 12, 'type': 'add', 'quantity': 2, 'side': 'sell',
              'price': 54}
        q4 = {'order_id': 't102_1', 'timestamp': 13, 'type': 'add', 'quantity': 3, 'side': 'buy',
              'price': 54}
        self.ex1.process_order(q3)
        self.assertEqual(len(self.ex1._ask_book_prices), 2)
        self.assertTrue(55 in self.ex1._ask_book_prices)
        self.assertTrue(54 in self.ex1._ask_book_prices)
        self.assertEqual(self.ex1._ask_book_prices[0], 54)
        self.assertEqual(self.ex1._ask_book_prices[1], 55)
        self.ex1.process_order(q4)
        self.assertEqual(len(self.ex1._ask_book_prices), 1)
        self.assertFalse(54 in self.ex1._ask_book_prices)
        self.assertTrue(55 in self.ex1._ask_book_prices)
        self.assertEqual(len(self.ex1._bid_book_prices), 4)
        self.assertTrue(54 in self.ex1._bid_book_prices)
        self.assertEqual(self.ex1._ask_book_prices[0], 55)
        self.assertEqual(self.ex1._bid_book_prices[-1], 54)

There is also an additional test for market collapse:

    def test_market_collapse(self):
        '''
        At setup(), there is 8 total bid size and 8 total ask size
        A trade for 8 or more should collapse the market
        '''
        print('Market Collapse Tests to stdout:\n')
        # seed order book
        self.ex1.add_order_to_book(self.q1_buy)
        self.ex1.add_order_to_book(self.q1_sell)
        # process new orders
        self.ex1.process_order(self.q2_buy)
        self.ex1.process_order(self.q2_sell)
        self.ex1.process_order(self.q3_buy)
        self.ex1.process_order(self.q3_sell)
        self.ex1.process_order(self.q4_buy)
        self.ex1.process_order(self.q4_sell)
        # The book: bids: 2@50, 3@49, 3@47 ; asks: 2@52, 3@53, 3@55
        # market buy order takes out part of the asks: no collapse
        q1 = {'order_id': 't100_1', 'timestamp': 10, 'type': 'add', 'quantity': 4, 'side': 'buy',
              'price': 100000}
        self.ex1.process_order(q1)
        # next market buy order takes out the asks: market collapse
        q2 = {'order_id': 't100_2', 'timestamp': 10, 'type': 'add', 'quantity': 5, 'side': 'buy',
              'price': 100000}
        self.ex1.process_order(q2)
        # market sell order takes out part of the bids: no collapse
        q3 = {'order_id': 't100_3', 'timestamp': 10, 'type': 'add', 'quantity': 4, 'side': 'sell',
              'price': 0}
        self.ex1.process_order(q3)
        # next market sell order takes out the asks: market collapse
        q4 = {'order_id': 't100_4', 'timestamp': 10, 'type': 'add', 'quantity': 5, 'side': 'sell',
              'price': 0}
        self.ex1.process_order(q4)

This test is mostly for internal checking. Many combinations of inputs will necessarily result in market collapse (i.e., exhaustion of all orders on one side of the book). Check out the Preis et al. (2006) and Preis et al. (2007) references in the Bibliography for more details. The final test checks for posting the top-of-book:

    def test_report_top_of_book(self):
        '''
        At setup(), top of book has 2 to sell at 52 and 2 to buy at 50
        at time = 3
        '''
        self.ex1.add_order_to_book(self.q1_buy)
        self.ex1.add_order_to_book(self.q2_buy)
        self.ex1.add_order_to_book(self.q1_sell)
        self.ex1.add_order_to_book(self.q2_sell)
        tob_check = {'timestamp': 5, 'best_bid': 50, 'best_ask': 52, 'bid_size': 2, 'ask_size': 2}
        self.ex1.report_top_of_book(5)
        self.assertDictEqual(self.ex1._sip_collector[0], tob_check)

That’s it for testing the Orderbook class and associated methods. As usual, the tests are more than twice as long as the actual code to be tested. Future posts will cover the @unittest.skip() decorator and how to run tests in a loop. If you have comments or suggestions, feel free to post them. The current WordPress settings require me to approve the first comment from a specific source. After that, you are free to comment without further approval. Coming up are posts describing the Trader classes and associated tests followed by a post or two on the simulation loop.

Published by

Chuck Collver

Quant, Programmer, Data Scientist, Developer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s