├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST ├── MANIFEST.in ├── Makefile ├── README.md ├── lightmatchingengine ├── __init__.py └── lightmatchingengine.pyx ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── performance │ └── performance_test.py └── unit │ └── test_basic_orders.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | *.c 6 | 7 | # Packages 8 | *.eggs 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Virtualenv 40 | env 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | - 3.4 6 | - 3.5 7 | - 3.6 8 | - 3.7 9 | - pypy 10 | 11 | install: 12 | - pip install .[performance] 13 | 14 | script: 15 | - make test 16 | - python tests/performance/performance_test.py --freq 20 --num-orders 500 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Gavin Chan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | lightmatchingengine/__init__.py 5 | lightmatchingengine/lightmatchingengine.py 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include *.py 2 | 3 | global-exclude __pycache__/* 4 | global-exclude .deps/* 5 | global-exclude *.so 6 | global-exclude *.pyd 7 | global-exclude *.pyc 8 | global-exclude .git* -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 lightmatchingengine tests 55 | 56 | test: ## run tests quickly with the default Python 57 | py.test tests/unit 58 | 59 | test-all: ## run tests on every Python version with tox 60 | tox 61 | 62 | coverage: ## check code coverage quickly with the default Python 63 | coverage run --source lightmatchingengine -m pytest 64 | coverage report -m 65 | coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | rm -f docs/lightmatchingengine.rst 70 | rm -f docs/modules.rst 71 | sphinx-apidoc -o docs/ lightmatchingengine 72 | $(MAKE) -C docs clean 73 | $(MAKE) -C docs html 74 | $(BROWSER) docs/_build/html/index.html 75 | 76 | servedocs: docs ## compile the docs watching for changes 77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 78 | 79 | release: dist ## package and upload a release 80 | twine upload dist/* 81 | 82 | dist: clean ## builds source and wheel package 83 | python setup.py sdist 84 | python setup.py bdist_wheel 85 | ls -l dist 86 | 87 | install: clean ## install the package to the active Python's site-packages 88 | python setup.py install 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightMatchingEngine 2 | 3 | A light matching engine written in Python. 4 | 5 | The engine is a trivial object to support 6 | 7 | * Add order - Returns the order and filled trades 8 | * Cancel order - Returns the original order 9 | 10 | The objective is to provide a easy interface for users on the standard 11 | price-time priority matching algorithm among different instruments. 12 | 13 | ## Installation 14 | 15 | The package can be installed by: 16 | 17 | ``` 18 | pip install lightmatchingengine 19 | ``` 20 | 21 | ## Usage 22 | 23 | Create a matching engine instance. 24 | 25 | ``` 26 | from lightmatchingengine.lightmatchingengine import LightMatchingEngine, Side 27 | 28 | lme = LightMatchingEngine() 29 | ``` 30 | 31 | Place an order. 32 | 33 | ``` 34 | order, trades = lme.add_order("EUR/USD", 1.10, 1000, Side.BUY) 35 | ``` 36 | 37 | Cancel an order. 38 | 39 | ``` 40 | del_order = lme.cancel_order(order.order_id, order.instmt) 41 | ``` 42 | 43 | Fill an order. 44 | 45 | ``` 46 | buy_order, trades = lme.add_order("EUR/USD", 1.10, 1000, Side.BUY) 47 | print("Number of trades = %d" % len(trades)) # Number of trades = 0 48 | print("Buy order quantity = %d" % buy_order.qty) # Buy order quantity = 1000 49 | print("Buy order filled = %d" % buy_order.cum_qty) # Buy order filled = 0 50 | print("Buy order leaves = %d" % buy_order.leaves_qty) # Buy order leaves = 1000 51 | 52 | sell_order, trades = lme.add_order("EUR/USD", 1.10, 1000, Side.SELL) 53 | print("Number of trades = %d" % len(trades)) # Number of trades = 2 54 | print("Buy order quantity = %d" % buy_order.qty) # Buy order quantity = 1000 55 | print("Buy order filled = %d" % buy_order.cum_qty) # Buy order filled = 1000 56 | print("Buy order leaves = %d" % buy_order.leaves_qty) # Buy order leaves = 0 57 | print("Trade price = %.2f" % trades[0].trade_price) # Trade price = 1.10 58 | print("Trade quantity = %d" % trades[0].trade_qty) # Trade quantity = 1000 59 | print("Trade side = %d" % trades[0].trade_side) # Trade side = 2 60 | 61 | ``` 62 | 63 | Failing to delete an order returns a None value. 64 | 65 | ``` 66 | del_order = lme.cancel_order(9999, order.instmt) 67 | print("Is order deleted = %d" % (del_order is not None)) # Is order deleted = 0 68 | ``` 69 | 70 | ## Supported version 71 | 72 | Python 2.x and 3.x are both supported. 73 | 74 | ## Order 75 | 76 | The order object contains the following information: 77 | 78 | * Exchange order ID (order_id) 79 | * Instrument name (instmt) 80 | * Price (price) 81 | * Quantity (qty) 82 | * Side (Buy/Sell) (side) 83 | * Cumulated filled quantity (cum_qty) 84 | * Leaves quantity (leaves_qty) 85 | 86 | ## Trade 87 | 88 | The trade object contains the following information: 89 | 90 | * Trade ID (trade_id) 91 | * Instrument name (instmt) 92 | * Exchange order ID (order_id) 93 | * Trade price (trade_price) 94 | * Trade quantity (trade_qty) 95 | * Trade side (trade_side) 96 | 97 | ## Performance 98 | 99 | To run the performance test, run the commands 100 | 101 | ``` 102 | pip install lightmatchingengine[performance] 103 | python tests/performance/performance_test.py --freq 20 104 | ``` 105 | 106 | It returns the latency in nanosecond like below 107 | 108 | | | add | cancel | add (trade > 0) | add (trade > 2.0) | 109 | |:------|---------:|---------:|------------------:|--------------------:| 110 | | count | 100 | 61 | 27 | 6 | 111 | | mean | 107.954 | 50.3532 | 164.412 | 205.437 | 112 | | std | 58.1438 | 16.3396 | 36.412 | 24.176 | 113 | | min | 17.1661 | 11.4441 | 74.1482 | 183.105 | 114 | | 25% | 81.3007 | 51.9753 | 141.382 | 188.47 | 115 | | 50% | 92.5064 | 58.4126 | 152.349 | 200.748 | 116 | | 75% | 140.19 | 59.3662 | 190.496 | 211.239 | 117 | | max | 445.604 | 71.0487 | 248.909 | 248.909 | 118 | 119 | 120 | ## Contact 121 | 122 | For any inquiries, please feel free to contact me by gavincyi at gmail dot com. 123 | -------------------------------------------------------------------------------- /lightmatchingengine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavincyi/LightMatchingEngine/5e210a809e62a802107831d0ca12498ed32d4717/lightmatchingengine/__init__.py -------------------------------------------------------------------------------- /lightmatchingengine/lightmatchingengine.pyx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | cpdef enum Side: 3 | BUY = 1 4 | SELL = 2 5 | 6 | 7 | cdef class Order: 8 | cdef public int order_id 9 | cdef public str instmt 10 | cdef public double price 11 | cdef public double qty 12 | cdef public double cum_qty 13 | cdef public double leaves_qty 14 | cdef public Side side 15 | 16 | def __init__(self, order_id, instmt, price, qty, side): 17 | """ 18 | Constructor 19 | """ 20 | self.order_id = order_id 21 | self.instmt = instmt 22 | self.price = price 23 | self.qty = qty 24 | self.cum_qty = 0 25 | self.leaves_qty = qty 26 | self.side = side 27 | 28 | 29 | cdef class OrderBook: 30 | cdef public dict bids 31 | cdef public dict asks 32 | cdef public dict order_id_map 33 | 34 | def __init__(self): 35 | """ 36 | Constructor 37 | """ 38 | self.bids = {} 39 | self.asks = {} 40 | self.order_id_map = {} 41 | 42 | 43 | cdef class Trade: 44 | cdef public int order_id 45 | cdef public str instmt 46 | cdef public double trade_price 47 | cdef public double trade_qty 48 | cdef public Side trade_side 49 | cdef public int trade_id 50 | 51 | def __init__(self, order_id, instmt, trade_price, trade_qty, trade_side, trade_id): 52 | """ 53 | Constructor 54 | """ 55 | self.order_id = order_id 56 | self.instmt = instmt 57 | self.trade_price = trade_price 58 | self.trade_qty = trade_qty 59 | self.trade_side = trade_side 60 | self.trade_id = trade_id 61 | 62 | 63 | cdef class LightMatchingEngine: 64 | cdef public dict order_books 65 | cdef public int curr_order_id 66 | cdef public int curr_trade_id 67 | 68 | def __init__(self): 69 | """ 70 | Constructor 71 | """ 72 | self.order_books = {} 73 | self.curr_order_id = 0 74 | self.curr_trade_id = 0 75 | 76 | cpdef add_order(self, str instmt, double price, double qty, Side side): 77 | """ 78 | Add an order 79 | :param instmt Instrument name 80 | :param price Price, defined as zero if market order 81 | :param qty Order quantity 82 | :param side 1 for BUY, 2 for SELL. Defaulted as BUY. 83 | :return The order and the list of trades. 84 | Empty list if there is no matching. 85 | """ 86 | cdef list trades = [] 87 | cdef int order_id 88 | cdef Order order 89 | 90 | assert side == Side.BUY or side == Side.SELL, \ 91 | "Invalid side %s" % side 92 | 93 | # Locate the order book 94 | order_book = self.order_books.setdefault(instmt, OrderBook()) 95 | 96 | # Initialization 97 | self.curr_order_id += 1 98 | order_id = self.curr_order_id 99 | order = Order(order_id, instmt, price, qty, side) 100 | 101 | if side == Side.BUY: 102 | # Buy 103 | best_price = min(order_book.asks.keys()) if len(order_book.asks) > 0 \ 104 | else None 105 | while best_price is not None and \ 106 | (price == 0.0 or price >= best_price ) and \ 107 | order.leaves_qty >= 1e-9: 108 | best_price_qty = sum([ask.leaves_qty for ask in order_book.asks[best_price]]) 109 | match_qty = min(best_price_qty, order.leaves_qty) 110 | assert match_qty >= 1e-9, "Match quantity must be larger than zero" 111 | 112 | # Generate aggressive order trade first 113 | self.curr_trade_id += 1 114 | order.cum_qty += match_qty 115 | order.leaves_qty -= match_qty 116 | trades.append(Trade(order_id, instmt, best_price, match_qty, \ 117 | Side.BUY, self.curr_trade_id)) 118 | 119 | # Generate the passive executions 120 | while match_qty >= 1e-9: 121 | # The order hit 122 | hit_order = order_book.asks[best_price][0] 123 | # The order quantity hit 124 | order_match_qty = min(match_qty, hit_order.leaves_qty) 125 | self.curr_trade_id += 1 126 | trades.append(Trade(hit_order.order_id, instmt, best_price, \ 127 | order_match_qty, \ 128 | Side.SELL, self.curr_trade_id)) 129 | hit_order.cum_qty += order_match_qty 130 | hit_order.leaves_qty -= order_match_qty 131 | match_qty -= order_match_qty 132 | if hit_order.leaves_qty < 1e-9: 133 | del order_book.asks[best_price][0] 134 | 135 | # If the price does not have orders, delete the particular price depth 136 | if len(order_book.asks[best_price]) == 0: 137 | del order_book.asks[best_price] 138 | 139 | # Update the best price 140 | best_price = min(order_book.asks.keys()) if len(order_book.asks) > 0 \ 141 | else None 142 | 143 | # Add the remaining order into the depth 144 | if order.leaves_qty >= 1e-9: 145 | depth = order_book.bids.setdefault(price, []) 146 | depth.append(order) 147 | order_book.order_id_map[order_id] = order 148 | else: 149 | #Sell 150 | best_price = max(order_book.bids.keys()) if len(order_book.bids) > 0 \ 151 | else None 152 | while best_price is not None and \ 153 | (price == 0.0 or price <= best_price) and \ 154 | order.leaves_qty >= 1e-9: 155 | best_price_qty = sum([bid.leaves_qty for bid in order_book.bids[best_price]]) 156 | match_qty = min(best_price_qty, order.leaves_qty) 157 | assert match_qty >= 1e-9, "Match quantity must be larger than zero" 158 | 159 | # Generate aggressive order trade first 160 | self.curr_trade_id += 1 161 | order.cum_qty += match_qty 162 | order.leaves_qty -= match_qty 163 | trades.append(Trade(order_id, instmt, best_price, match_qty, \ 164 | Side.SELL, self.curr_trade_id)) 165 | 166 | # Generate the passive executions 167 | while match_qty >= 1e-9: 168 | # The order hit 169 | hit_order = order_book.bids[best_price][0] 170 | # The order quantity hit 171 | order_match_qty = min(match_qty, hit_order.leaves_qty) 172 | self.curr_trade_id += 1 173 | trades.append(Trade(hit_order.order_id, instmt, best_price, \ 174 | order_match_qty, \ 175 | Side.BUY, self.curr_trade_id)) 176 | hit_order.cum_qty += order_match_qty 177 | hit_order.leaves_qty -= order_match_qty 178 | match_qty -= order_match_qty 179 | if hit_order.leaves_qty < 1e-9: 180 | del order_book.bids[best_price][0] 181 | 182 | # If the price does not have orders, delete the particular price depth 183 | if len(order_book.bids[best_price]) == 0: 184 | del order_book.bids[best_price] 185 | 186 | # Update the best price 187 | best_price = max(order_book.bids.keys()) if len(order_book.bids) > 0 \ 188 | else None 189 | 190 | # Add the remaining order into the depth 191 | if order.leaves_qty >= 1e-9: 192 | depth = order_book.asks.setdefault(price, []) 193 | depth.append(order) 194 | order_book.order_id_map[order_id] = order 195 | 196 | return order, trades 197 | 198 | cpdef cancel_order(self, int order_id, str instmt): 199 | """ 200 | Cancel order 201 | :param order_id Order ID 202 | :param instmt Instrument 203 | :return The order if the cancellation is successful 204 | """ 205 | cdef Order order 206 | cdef double order_price 207 | cdef Side side 208 | cdef int index 209 | 210 | assert instmt in self.order_books.keys(), \ 211 | "Instrument %s is not valid in the order book" % instmt 212 | order_book = self.order_books[instmt] 213 | 214 | if order_id not in order_book.order_id_map.keys(): 215 | # Invalid order id 216 | return None 217 | 218 | order = order_book.order_id_map[order_id] 219 | order_price = order.price 220 | order_id = order.order_id 221 | side = order.side 222 | 223 | if side == Side.BUY: 224 | assert order_price in order_book.bids.keys(), \ 225 | "Order price %.6f is not in the bid price depth" % order_price 226 | price_level = order_book.bids[order_price] 227 | else: 228 | assert order_price in order_book.asks.keys(), \ 229 | "Order price %.6f is not in the ask price depth" % order_price 230 | price_level = order_book.asks[order_price] 231 | 232 | index = 0 233 | price_level_len = len(price_level) 234 | while index < price_level_len: 235 | if price_level[index].order_id == order_id: 236 | del price_level[index] 237 | break 238 | index += 1 239 | 240 | if index == price_level_len: 241 | # Cannot find the order ID. Incorrect side 242 | return None 243 | 244 | if side == Side.BUY and len(order_book.bids[order_price]) == 0: 245 | # Delete empty particular price level 246 | del order_book.bids[order_price] 247 | elif side == Side.SELL and len(order_book.asks[order_price]) == 0: 248 | # Delete empty particular price level 249 | del order_book.asks[order_price] 250 | 251 | # Delete the order id from the map 252 | del order_book.order_id_map[order_id] 253 | 254 | # Zero out leaves qty 255 | order.leaves_qty = 0 256 | 257 | return order 258 | 259 | cpdef amend_order(self, int order_id, str instmt, double amended_price, 260 | double amended_qty): 261 | """ 262 | Amend an order 263 | :param order_id Order ID 264 | :param instmt Instrument 265 | :param amended_price Amended price, defined as zero if market order 266 | :param amended_qty Amended order quantity 267 | :return The order and the list of trades. 268 | Empty list if there is no matching. 269 | """ 270 | cdef Order order 271 | cdef double order_price 272 | cdef Side side 273 | cdef int index 274 | 275 | assert instmt in self.order_books.keys(), \ 276 | "Instrument %s is not valid in the order book" % instmt 277 | order_book = self.order_books[instmt] 278 | 279 | if order_id not in order_book.order_id_map.keys(): 280 | # Invalid order id 281 | return None 282 | 283 | order = order_book.order_id_map[order_id] 284 | order_price = order.price 285 | 286 | assert amended_qty - order.cum_qty >= 1e-9, ( 287 | "The amended qty (%s) cannot be amended below the cum qty (%s)" 288 | % (amended_qty, order.cum_qty) 289 | ) 290 | 291 | if (abs(order_price - amended_price) < 1e-9 and 292 | amended_qty <= order.qty): 293 | # The priority queue is not changed as the quantity of the 294 | # order is reduced 295 | order.leaves_qty -= (order.qty - amended_qty) 296 | order.qty = amended_qty 297 | 298 | # Return amended order without any trades 299 | return order, [] 300 | 301 | # Otherwise cancel the order and add a new order 302 | old_order = self.cancel_order(order_id=order_id, instmt=instmt) 303 | 304 | assert old_order is not None, ( 305 | "Failed to cancel the order id %s" % order_id 306 | ) 307 | 308 | return self.add_order( 309 | instmt=instmt, 310 | price=amended_price, 311 | qty=amended_qty, 312 | side=order.side 313 | ) 314 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages, Extension 2 | 3 | setup( 4 | name="lightmatchingengine", 5 | url="https://github.com/gavincyi/LightMatchingEngine", 6 | license='MIT', 7 | 8 | author="Gavin Chan", 9 | author_email="gavincyi@gmail.com", 10 | 11 | description="A light matching engine", 12 | 13 | packages=find_packages(exclude=('tests',)), 14 | 15 | use_scm_version=True, 16 | install_requires=[], 17 | setup_requires=['setuptools_scm', 'cython'], 18 | ext_modules=[Extension( 19 | 'lightmatchingengine.lightmatchingengine', 20 | ['lightmatchingengine/lightmatchingengine.pyx'])], 21 | tests_require=[ 22 | 'pytest' 23 | ], 24 | extras_require={ 25 | 'performance': ['pandas', 'docopt', 'tabulate', 'tqdm'] 26 | }, 27 | 28 | classifiers=[ 29 | 'Development Status :: 2 - Pre-Alpha', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Programming Language :: Python', 32 | 'Programming Language :: Python :: 2', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.4', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | ], 40 | 41 | ) 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavincyi/LightMatchingEngine/5e210a809e62a802107831d0ca12498ed32d4717/tests/__init__.py -------------------------------------------------------------------------------- /tests/performance/performance_test.py: -------------------------------------------------------------------------------- 1 | """Performance test for light matching engine. 2 | 3 | Usage: 4 | perf-test-light-matching-engine --freq [options] 5 | 6 | Options: 7 | -h --help Show help. 8 | --freq= Order frequency per second. [Default: 10] 9 | --num-orders= Number of orders. [Default: 100] 10 | --add-order-prob= Add order probability. [Default: 0.6] 11 | --mean-price= Mean price in the standard normal distribution. 12 | [Default: 100] 13 | --std-price= Standard derivation of the price in the standing 14 | derivation. [Default: 0.5] 15 | --tick-size= Tick size. [Default: 0.1] 16 | --gamma-quantity= Gamma value in the gamma distribution for the 17 | order quantity. [Default: 2] 18 | """ 19 | from docopt import docopt 20 | import logging 21 | from math import log 22 | from random import uniform, seed 23 | from time import sleep, time 24 | 25 | from tabulate import tabulate 26 | from tqdm import tqdm 27 | 28 | import numpy as np 29 | import pandas as pd 30 | 31 | from lightmatchingengine.lightmatchingengine import ( 32 | LightMatchingEngine, Side) 33 | 34 | LOGGER = logging.getLogger(__name__) 35 | 36 | 37 | class Timer: 38 | def __enter__(self): 39 | self.start = time() 40 | return self 41 | 42 | def __exit__(self, *args): 43 | self.end = time() 44 | self.interval = self.end - self.start 45 | 46 | 47 | def run(args): 48 | engine = LightMatchingEngine() 49 | 50 | symbol = "EUR/USD" 51 | add_order_prob = float(args['--add-order-prob']) 52 | num_of_orders = int(args['--num-orders']) 53 | gamma_quantity = float(args['--gamma-quantity']) 54 | mean_price = float(args['--mean-price']) 55 | std_price = float(args['--std-price']) 56 | tick_size = float(args['--tick-size']) 57 | freq = float(args['--freq']) 58 | orders = {} 59 | add_statistics = [] 60 | cancel_statistics = [] 61 | 62 | # Initialize random seed 63 | seed(42) 64 | 65 | progress_bar = tqdm(num_of_orders) 66 | while num_of_orders > 0: 67 | if uniform(0, 1) <= add_order_prob or len(orders) == 0: 68 | price = np.random.standard_normal() * std_price + mean_price 69 | price = int(price / tick_size) * tick_size 70 | quantity = np.random.gamma(gamma_quantity) + 1 71 | side = Side.BUY if uniform(0, 1) <= 0.5 else Side.SELL 72 | 73 | # Add the order 74 | with Timer() as timer: 75 | order, trades = engine.add_order(symbol, price, quantity, side) 76 | 77 | LOGGER.debug('Order %s is added at side %s, price %s ' 78 | 'and quantity %s', 79 | order.order_id, order.side, order.price, order.qty) 80 | 81 | # Save the order if there is any quantity left 82 | if order.leaves_qty > 0.0: 83 | orders[order.order_id] = order 84 | 85 | # Remove the trades 86 | for trade in trades: 87 | if (trade.order_id != order.order_id and 88 | orders[trade.order_id].leaves_qty < 1e-9): 89 | del orders[trade.order_id] 90 | 91 | # Save the statistics 92 | add_statistics.append((order, len(trades), timer)) 93 | 94 | num_of_orders -= 1 95 | progress_bar.update(1) 96 | else: 97 | index = int(uniform(0, 1) * len(orders)) 98 | if index == len(orders): 99 | index -= 1 100 | 101 | order_id = list(orders.keys())[index] 102 | 103 | with Timer() as timer: 104 | engine.cancel_order(order_id, order.instmt) 105 | 106 | LOGGER.debug('Order %s is deleted', order_id) 107 | del orders[order_id] 108 | 109 | # Save the statistics 110 | cancel_statistics.append((order, timer)) 111 | 112 | # Next time = -ln(U) / lambda 113 | sleep(-log(uniform(0, 1)) / freq) 114 | 115 | return add_statistics, cancel_statistics 116 | 117 | 118 | def describe_statistics(add_statistics, cancel_statistics): 119 | add_statistics = pd.DataFrame([ 120 | (trade_num, timer.interval * 1e6) 121 | for _, trade_num, timer in add_statistics], 122 | columns=['trade_num', 'interval']) 123 | 124 | # Trade statistics 125 | trade_statistics = add_statistics['trade_num'].describe() 126 | LOGGER.info('Trade statistics:\n%s', 127 | tabulate(trade_statistics.to_frame(name='trade'), 128 | tablefmt='pipe')) 129 | 130 | cancel_statistics = pd.Series([ 131 | timer.interval * 1e6 for _, timer in cancel_statistics], 132 | name='interval') 133 | 134 | statistics = pd.concat([ 135 | add_statistics['interval'].describe(), 136 | cancel_statistics.describe()], 137 | keys=['add', 'cancel'], 138 | axis=1) 139 | 140 | statistics['add (trade > 0)'] = ( 141 | add_statistics.loc[ 142 | add_statistics['trade_num'] > 0, 'interval'].describe()) 143 | 144 | percentile_75 = trade_statistics['75%'] 145 | statistics['add (trade > %s)' % percentile_75] = ( 146 | add_statistics.loc[add_statistics['trade_num'] > percentile_75, 147 | 'interval'].describe()) 148 | 149 | LOGGER.info('Matching engine latency (nanoseconds):\n%s', 150 | tabulate(statistics, 151 | headers=statistics.columns, 152 | tablefmt='pipe')) 153 | 154 | if __name__ == '__main__': 155 | args = docopt(__doc__, version='1.0.0') 156 | logging.basicConfig(level=logging.INFO) 157 | 158 | LOGGER.info('Running the performance benchmark') 159 | add_statistics, cancel_statistics = run(args) 160 | describe_statistics(add_statistics, cancel_statistics) 161 | -------------------------------------------------------------------------------- /tests/unit/test_basic_orders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import lightmatchingengine.lightmatchingengine as lme 3 | import unittest 4 | 5 | class TestBasicOrders(unittest.TestCase): 6 | instmt = "TestingInstrument" 7 | price = 100.0 8 | lot_size = 1.0 9 | 10 | def check_order(self, order, order_id, instmt, price, qty, side, cum_qty, leaves_qty): 11 | """ 12 | Check the order information 13 | """ 14 | self.assertTrue(order is not None) 15 | self.assertEqual(order_id, order.order_id) 16 | self.assertEqual(instmt, order.instmt) 17 | self.assertEqual(price, order.price) 18 | self.assertEqual(qty, order.qty) 19 | self.assertEqual(side, order.side) 20 | self.assertEqual(cum_qty, order.cum_qty) 21 | self.assertEqual(leaves_qty, order.leaves_qty) 22 | 23 | def check_trade(self, trade, order_id, instmt, trade_price, trade_qty, trade_side, trade_id): 24 | """ 25 | Check the trade information 26 | """ 27 | self.assertTrue(trade is not None) 28 | self.assertEqual(order_id, trade.order_id) 29 | self.assertEqual(instmt, trade.instmt) 30 | self.assertEqual(trade_price, trade.trade_price) 31 | self.assertEqual(trade_qty, trade.trade_qty) 32 | self.assertEqual(trade_side, trade.trade_side) 33 | self.assertEqual(trade_id, trade.trade_id) 34 | 35 | def check_order_book(self, me, instmt, num_bids_level, num_asks_level): 36 | """ 37 | Check the order book depth 38 | """ 39 | self.assertTrue(instmt in me.order_books.keys()) 40 | self.assertEqual(num_bids_level, len(me.order_books[instmt].bids)) 41 | self.assertEqual(num_asks_level, len(me.order_books[instmt].asks)) 42 | 43 | def check_deleted_order(self, order, del_order): 44 | """ 45 | Check if the deleted order is same as the original order 46 | """ 47 | self.assertTrue(order is not None) 48 | self.assertTrue(del_order is not None) 49 | self.assertEqual(order, del_order) 50 | self.assertEqual(0, del_order.leaves_qty) 51 | 52 | def test_cancel_order(self): 53 | me = lme.LightMatchingEngine() 54 | 55 | # Place a buy order 56 | order, trades = me.add_order(TestBasicOrders.instmt, \ 57 | TestBasicOrders.price, \ 58 | TestBasicOrders.lot_size, \ 59 | lme.Side.BUY) 60 | self.assertEqual(0, len(trades)) 61 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 62 | self.check_order(order, 1, TestBasicOrders.instmt, TestBasicOrders.price, \ 63 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 64 | 65 | # Cancel a buy order 66 | del_order = me.cancel_order(order.order_id, TestBasicOrders.instmt) 67 | self.assertEqual(0, len(trades)) 68 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 69 | self.check_deleted_order(order, del_order) 70 | 71 | # Place a sell order 72 | order, trades = me.add_order(TestBasicOrders.instmt, \ 73 | TestBasicOrders.price, \ 74 | TestBasicOrders.lot_size, \ 75 | lme.Side.SELL) 76 | self.assertEqual(0, len(trades)) 77 | self.check_order_book(me, TestBasicOrders.instmt, 0, 1) 78 | self.check_order(order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ 79 | TestBasicOrders.lot_size, lme.Side.SELL, 0, TestBasicOrders.lot_size) 80 | 81 | # Cancel a sell order 82 | del_order = me.cancel_order(order.order_id, TestBasicOrders.instmt) 83 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 84 | self.check_deleted_order(order, del_order) 85 | 86 | def test_fill_order(self): 87 | me = lme.LightMatchingEngine() 88 | 89 | # Place a buy order 90 | buy_order, trades = me.add_order(TestBasicOrders.instmt, \ 91 | TestBasicOrders.price, \ 92 | TestBasicOrders.lot_size, \ 93 | lme.Side.BUY) 94 | self.assertEqual(0, len(trades)) 95 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 96 | self.check_order(buy_order, 1, TestBasicOrders.instmt, TestBasicOrders.price, \ 97 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 98 | 99 | # Place a sell order 100 | sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 101 | TestBasicOrders.price, \ 102 | TestBasicOrders.lot_size, \ 103 | lme.Side.SELL) 104 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 105 | self.assertEqual(2, len(trades)) 106 | self.check_order(buy_order, 1, TestBasicOrders.instmt, TestBasicOrders.price, \ 107 | TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) 108 | self.check_order(sell_order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ 109 | TestBasicOrders.lot_size, lme.Side.SELL, TestBasicOrders.lot_size, 0) 110 | 111 | # Check trades 112 | self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ 113 | sell_order.price, sell_order.qty, sell_order.side, 1) 114 | self.check_trade(trades[1], buy_order.order_id, buy_order.instmt, \ 115 | buy_order.price, buy_order.qty, buy_order.side, 2) 116 | 117 | def test_fill_multiple_orders_same_level(self): 118 | me = lme.LightMatchingEngine() 119 | 120 | # Place buy orders 121 | for i in range(1, 11): 122 | buy_order, trades = me.add_order(TestBasicOrders.instmt, \ 123 | TestBasicOrders.price, \ 124 | TestBasicOrders.lot_size, \ 125 | lme.Side.BUY) 126 | self.assertEqual(0, len(trades)) 127 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 128 | self.assertEqual(i, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price])) 129 | self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price, \ 130 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 131 | 132 | # Place sell orders 133 | sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 134 | TestBasicOrders.price, \ 135 | 10.0 * TestBasicOrders.lot_size, \ 136 | lme.Side.SELL) 137 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 138 | self.assertEqual(11, len(trades)) 139 | self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price, \ 140 | TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) 141 | self.check_order(sell_order, 11, TestBasicOrders.instmt, TestBasicOrders.price, \ 142 | 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) 143 | 144 | # Check aggressive hit orders 145 | self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ 146 | sell_order.price, sell_order.qty, sell_order.side, 1) 147 | 148 | # Check passive hit orders 149 | for i in range(1, 11): 150 | self.check_trade(trades[i], i, buy_order.instmt, \ 151 | buy_order.price, buy_order.qty, buy_order.side, i+1) 152 | 153 | def test_fill_multiple_orders_different_level(self): 154 | me = lme.LightMatchingEngine() 155 | 156 | # Place buy orders 157 | for i in range(1, 11): 158 | buy_order, trades = me.add_order(TestBasicOrders.instmt, \ 159 | TestBasicOrders.price+i, \ 160 | TestBasicOrders.lot_size, \ 161 | lme.Side.BUY) 162 | self.assertEqual(0, len(trades)) 163 | self.check_order_book(me, TestBasicOrders.instmt, i, 0) 164 | self.assertEqual(1, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price+i])) 165 | self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price+i, \ 166 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 167 | 168 | # Place sell orders 169 | sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 170 | TestBasicOrders.price, \ 171 | 10.0 * TestBasicOrders.lot_size, \ 172 | lme.Side.SELL) 173 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 174 | self.assertEqual(20, len(trades)) 175 | self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price+10, \ 176 | TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) 177 | self.check_order(sell_order, 11, TestBasicOrders.instmt, TestBasicOrders.price, \ 178 | 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) 179 | 180 | for i in range(0, 10): 181 | match_price = sell_order.price+10-i 182 | self.check_trade(trades[2*i], sell_order.order_id, sell_order.instmt, \ 183 | match_price, TestBasicOrders.lot_size, sell_order.side, 2*i+1) 184 | self.check_trade(trades[2*i+1], 10-i, buy_order.instmt, \ 185 | match_price, TestBasicOrders.lot_size, buy_order.side, 2*i+2) 186 | 187 | def test_cancel_partial_fill_orders(self): 188 | me = lme.LightMatchingEngine() 189 | 190 | # Place a buy order 191 | buy1_order, trades = me.add_order(TestBasicOrders.instmt, \ 192 | TestBasicOrders.price + 0.1, \ 193 | TestBasicOrders.lot_size, \ 194 | lme.Side.BUY) 195 | self.assertEqual(0, len(trades)) 196 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 197 | self.check_order(buy1_order, 1, TestBasicOrders.instmt, TestBasicOrders.price + 0.1, \ 198 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 199 | 200 | # Place a buy order 201 | buy2_order, trades = me.add_order(TestBasicOrders.instmt, \ 202 | TestBasicOrders.price, \ 203 | 2 * TestBasicOrders.lot_size, \ 204 | lme.Side.BUY) 205 | self.assertEqual(0, len(trades)) 206 | self.check_order_book(me, TestBasicOrders.instmt, 2, 0) 207 | self.check_order(buy2_order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ 208 | 2 * TestBasicOrders.lot_size, lme.Side.BUY, 0, 2 * TestBasicOrders.lot_size) 209 | 210 | # Place a sell order 211 | sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 212 | TestBasicOrders.price, \ 213 | 2 * TestBasicOrders.lot_size, \ 214 | lme.Side.SELL) 215 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 216 | self.assertEqual(4, len(trades)) 217 | self.check_order(buy1_order, 1, TestBasicOrders.instmt, TestBasicOrders.price + 0.1, \ 218 | TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) 219 | self.check_order(buy2_order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ 220 | 2*TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, TestBasicOrders.lot_size) 221 | self.check_order(sell_order, 3, TestBasicOrders.instmt, TestBasicOrders.price, \ 222 | 2*TestBasicOrders.lot_size, lme.Side.SELL, 2*TestBasicOrders.lot_size, 0) 223 | 224 | # Check trades 225 | self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ 226 | TestBasicOrders.price + 0.1, TestBasicOrders.lot_size, sell_order.side, 1) 227 | self.check_trade(trades[1], buy1_order.order_id, buy1_order.instmt, \ 228 | TestBasicOrders.price + 0.1, TestBasicOrders.lot_size, buy1_order.side, 2) 229 | self.check_trade(trades[2], sell_order.order_id, sell_order.instmt, \ 230 | TestBasicOrders.price, TestBasicOrders.lot_size, sell_order.side, 3) 231 | self.check_trade(trades[3], buy2_order.order_id, buy1_order.instmt, \ 232 | TestBasicOrders.price, TestBasicOrders.lot_size, buy2_order.side, 4) 233 | 234 | # Cancel the second order 235 | del_order = me.cancel_order(buy2_order.order_id, TestBasicOrders.instmt) 236 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 237 | self.check_deleted_order(buy2_order, del_order) 238 | 239 | def test_fill_multiple_orders_same_level_market_order(self): 240 | me = lme.LightMatchingEngine() 241 | 242 | # Place buy orders 243 | for i in range(1, 11): 244 | buy_order, trades = me.add_order(TestBasicOrders.instmt, \ 245 | TestBasicOrders.price, \ 246 | TestBasicOrders.lot_size, \ 247 | lme.Side.BUY) 248 | self.assertEqual(0, len(trades)) 249 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 250 | self.assertEqual(i, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price])) 251 | self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price, \ 252 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 253 | 254 | # Place sell orders 255 | sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 256 | 0, \ 257 | 10.0 * TestBasicOrders.lot_size, \ 258 | lme.Side.SELL) 259 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 260 | self.assertEqual(11, len(trades)) 261 | self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price, \ 262 | TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) 263 | self.check_order(sell_order, 11, TestBasicOrders.instmt, 0, \ 264 | 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) 265 | 266 | # Check aggressive hit orders - Trade price is same as the passive hit limit price 267 | self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ 268 | buy_order.price, sell_order.qty, sell_order.side, 1) 269 | 270 | # Check passive hit orders 271 | for i in range(1, 11): 272 | self.check_trade(trades[i], i, buy_order.instmt, \ 273 | buy_order.price, buy_order.qty, buy_order.side, i+1) 274 | 275 | def test_fill_multiple_orders_different_level_market_order(self): 276 | me = lme.LightMatchingEngine() 277 | 278 | # Place buy orders 279 | for i in range(1, 11): 280 | buy_order, trades = me.add_order(TestBasicOrders.instmt, \ 281 | TestBasicOrders.price+i, \ 282 | TestBasicOrders.lot_size, \ 283 | lme.Side.BUY) 284 | self.assertEqual(0, len(trades)) 285 | self.check_order_book(me, TestBasicOrders.instmt, i, 0) 286 | self.assertEqual(1, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price+i])) 287 | self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price+i, \ 288 | TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) 289 | 290 | # Place sell orders 291 | sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 292 | 0, \ 293 | 10.0 * TestBasicOrders.lot_size, \ 294 | lme.Side.SELL) 295 | self.check_order_book(me, TestBasicOrders.instmt, 0, 0) 296 | self.assertEqual(20, len(trades)) 297 | self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price+10, \ 298 | TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) 299 | self.check_order(sell_order, 11, TestBasicOrders.instmt, 0, \ 300 | 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) 301 | 302 | for i in range(0, 10): 303 | match_price = TestBasicOrders.price+10-i 304 | self.check_trade(trades[2*i], sell_order.order_id, sell_order.instmt, \ 305 | match_price, TestBasicOrders.lot_size, sell_order.side, 2*i+1) 306 | self.check_trade(trades[2*i+1], 10-i, buy_order.instmt, \ 307 | match_price, TestBasicOrders.lot_size, buy_order.side, 2*i+2) 308 | 309 | def test_amend_qty_down(self): 310 | me = lme.LightMatchingEngine() 311 | 312 | # Place a buy order 313 | order, trades = me.add_order( 314 | instmt=TestBasicOrders.instmt, 315 | price=TestBasicOrders.price, 316 | qty=TestBasicOrders.lot_size * 3, 317 | side=lme.Side.BUY 318 | ) 319 | 320 | # Place another buy order 321 | order2, trades2 = me.add_order( 322 | instmt=TestBasicOrders.instmt, 323 | price=TestBasicOrders.price, 324 | qty=TestBasicOrders.lot_size, 325 | side=lme.Side.BUY 326 | ) 327 | 328 | # Check the order book and trades 329 | self.assertEqual(0, len(trades)) 330 | self.assertEqual(0, len(trades2)) 331 | self.check_order_book(me, TestBasicOrders.instmt, 1, 0) 332 | self.check_order( 333 | order=order, order_id=1, instmt=TestBasicOrders.instmt, 334 | price=TestBasicOrders.price, qty=TestBasicOrders.lot_size * 3, 335 | side=lme.Side.BUY, cum_qty=0, leaves_qty=TestBasicOrders.lot_size * 3) 336 | self.check_order( 337 | order=order2, order_id=2, instmt=TestBasicOrders.instmt, 338 | price=TestBasicOrders.price, qty=TestBasicOrders.lot_size, 339 | side=lme.Side.BUY, cum_qty=0, leaves_qty=TestBasicOrders.lot_size) 340 | 341 | # Partial fill the first order 342 | order3, trades3 = me.add_order( 343 | instmt=TestBasicOrders.instmt, 344 | price=TestBasicOrders.price, 345 | qty=TestBasicOrders.lot_size, 346 | side=lme.Side.SELL 347 | ) 348 | self.assertEqual(2, len(trades3)) 349 | self.check_trade( 350 | trade=trades3[0], order_id=3, instmt=TestBasicOrders.instmt, 351 | trade_price=TestBasicOrders.price, 352 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.SELL, 353 | trade_id=1) 354 | self.check_trade( 355 | trade=trades3[1], order_id=1, instmt=TestBasicOrders.instmt, 356 | trade_price=TestBasicOrders.price, 357 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.BUY, 358 | trade_id=2) 359 | self.check_order( 360 | order=order, order_id=1, instmt=TestBasicOrders.instmt, 361 | price=TestBasicOrders.price, qty=TestBasicOrders.lot_size * 3, 362 | side=lme.Side.BUY, cum_qty=TestBasicOrders.lot_size, 363 | leaves_qty=TestBasicOrders.lot_size * 2) 364 | 365 | # Amend the quantity down 366 | order, trades = me.amend_order( 367 | order_id=order.order_id, 368 | instmt=TestBasicOrders.instmt, 369 | amended_price=TestBasicOrders.price, 370 | amended_qty=TestBasicOrders.lot_size * 2, 371 | ) 372 | self.assertEqual(0, len(trades)) 373 | self.check_order( 374 | order=order, order_id=1, instmt=TestBasicOrders.instmt, 375 | price=TestBasicOrders.price, qty=TestBasicOrders.lot_size * 2, 376 | side=lme.Side.BUY, cum_qty=TestBasicOrders.lot_size, 377 | leaves_qty=TestBasicOrders.lot_size) 378 | 379 | # Fill the remaining orders 380 | order4, trades4 = me.add_order( 381 | instmt=TestBasicOrders.instmt, 382 | price=TestBasicOrders.price, 383 | qty=TestBasicOrders.lot_size * 2, 384 | side=lme.Side.SELL 385 | ) 386 | self.assertEqual(3, len(trades4)) 387 | self.check_order( 388 | order=order4, order_id=4, instmt=TestBasicOrders.instmt, 389 | price=TestBasicOrders.price, qty=TestBasicOrders.lot_size * 2, 390 | side=lme.Side.SELL, cum_qty=TestBasicOrders.lot_size * 2, 391 | leaves_qty=0.0) 392 | self.check_trade( 393 | trade=trades4[0], order_id=4, instmt=TestBasicOrders.instmt, 394 | trade_price=TestBasicOrders.price, 395 | trade_qty=TestBasicOrders.lot_size * 2, trade_side=lme.Side.SELL, 396 | trade_id=3) 397 | self.check_trade( 398 | trade=trades4[1], order_id=1, instmt=TestBasicOrders.instmt, 399 | trade_price=TestBasicOrders.price, 400 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.BUY, 401 | trade_id=4) 402 | self.check_trade( 403 | trade=trades4[2], order_id=2, instmt=TestBasicOrders.instmt, 404 | trade_price=TestBasicOrders.price, 405 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.BUY, 406 | trade_id=5) 407 | 408 | def test_amend_order_price_and_qty(self): 409 | """Test the amend order price and qty. 410 | 411 | 1. Place two buy orders on the same price (id = 1 and id = 2) 412 | 2. Place two sell orders of which one is 0.1 higher than another 413 | (id = 3 and id = 4). 414 | 3. Amend on buy order (id = 2 => 5) price and the qty from the back 415 | to execute on the best ask (id = 3). 416 | 4. Amend the buy order (id = 1 => 6) to the best bid price. 417 | 5. Amend the front best bid order (id = 5 => 7) quantity up. The 418 | original order quantity is 2 * lot_size and the leaves qty is 419 | lot_size. Amending the volume up from 2 to 3 creates a new order 420 | with order quantity = 3. 421 | 6. Amend the sell order (id = 4) to execute the best bid orders, 422 | the first matched buy order should be with id = 6 and then id = 7. 423 | """ 424 | me = lme.LightMatchingEngine() 425 | 426 | # 1. Place two buy orders on the same price (id = 1 and id = 2) 427 | order, trades = me.add_order( 428 | instmt=TestBasicOrders.instmt, 429 | price=TestBasicOrders.price, 430 | qty=TestBasicOrders.lot_size, 431 | side=lme.Side.BUY 432 | ) 433 | 434 | # Place another buy order 435 | order2, trades2 = me.add_order( 436 | instmt=TestBasicOrders.instmt, 437 | price=TestBasicOrders.price, 438 | qty=TestBasicOrders.lot_size, 439 | side=lme.Side.BUY 440 | ) 441 | 442 | self.assertEqual(0, len(trades)) 443 | self.assertEqual(0, len(trades2)) 444 | 445 | # 2. Place two sell orders of which one is 0.1 higher than another 446 | # (id = 3 and id = 4). 447 | order3, trades3 = me.add_order( 448 | instmt=TestBasicOrders.instmt, 449 | price=TestBasicOrders.price + 0.1, 450 | qty=TestBasicOrders.lot_size, 451 | side=lme.Side.SELL 452 | ) 453 | order4, trades4 = me.add_order( 454 | instmt=TestBasicOrders.instmt, 455 | price=TestBasicOrders.price + 0.2, 456 | qty=TestBasicOrders.lot_size, 457 | side=lme.Side.SELL 458 | ) 459 | 460 | self.assertEqual(0, len(trades3)) 461 | self.assertEqual(0, len(trades4)) 462 | 463 | # 3. Amend on buy order (id = 2) price and the qty from the back 464 | # to execute on the best ask (id = 3). 465 | order5, trades5 = me.amend_order( 466 | instmt=TestBasicOrders.instmt, 467 | order_id=order2.order_id, 468 | amended_price=TestBasicOrders.price + 0.1, 469 | amended_qty=TestBasicOrders.lot_size * 2, 470 | ) 471 | 472 | # A new order id should be generated 473 | self.assertEqual(2, len(trades5)) 474 | self.check_order( 475 | order=order5, order_id=5, instmt=TestBasicOrders.instmt, 476 | price=TestBasicOrders.price + 0.1, qty=TestBasicOrders.lot_size * 2, 477 | side=lme.Side.BUY, cum_qty=TestBasicOrders.lot_size, 478 | leaves_qty=TestBasicOrders.lot_size) 479 | self.check_trade( 480 | trade=trades5[0], order_id=5, instmt=TestBasicOrders.instmt, 481 | trade_price=TestBasicOrders.price + 0.1, 482 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.BUY, 483 | trade_id=1) 484 | self.check_trade( 485 | trade=trades5[1], order_id=3, instmt=TestBasicOrders.instmt, 486 | trade_price=TestBasicOrders.price + 0.1, 487 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.SELL, 488 | trade_id=2) 489 | 490 | # 4. Amend the buy order (id = 1 => 6) to the best bid price. 491 | order6, trades6 = me.amend_order( 492 | instmt=TestBasicOrders.instmt, 493 | order_id=order.order_id, 494 | amended_price=TestBasicOrders.price + 0.1, 495 | amended_qty=TestBasicOrders.lot_size, 496 | ) 497 | 498 | self.assertEqual(0, len(trades6)) 499 | self.check_order( 500 | order=order6, order_id=6, instmt=TestBasicOrders.instmt, 501 | price=TestBasicOrders.price + 0.1, qty=TestBasicOrders.lot_size, 502 | side=lme.Side.BUY, cum_qty=0.0, 503 | leaves_qty=TestBasicOrders.lot_size) 504 | 505 | # 5. Amend the front best bid order (id = 5 => 7) quantity up. The 506 | # original order quantity is 2 * lot_size and the leaves qty is 507 | # lot_size. Amending the volume up from 2 to 3 creates a new order 508 | # with order quantity = 2 (new qty - original leaves qty = 3 - 1). 509 | order7, trades7 = me.amend_order( 510 | instmt=TestBasicOrders.instmt, 511 | order_id=order5.order_id, 512 | amended_price=TestBasicOrders.price + 0.1, 513 | amended_qty=TestBasicOrders.lot_size * 3, 514 | ) 515 | 516 | self.assertEqual(0, len(trades7)) 517 | self.check_order( 518 | order=order7, order_id=7, instmt=TestBasicOrders.instmt, 519 | price=TestBasicOrders.price + 0.1, qty=TestBasicOrders.lot_size * 3, 520 | side=lme.Side.BUY, cum_qty=0.0, 521 | leaves_qty=TestBasicOrders.lot_size * 3 522 | ) 523 | 524 | # 6. Amend the sell order (id = 4) to execute the best bid orders, 525 | # the first matched buy order should be with id = 6 and then id = 7. 526 | order8, trades8 = me.amend_order( 527 | instmt=TestBasicOrders.instmt, 528 | order_id=order4.order_id, 529 | amended_price=TestBasicOrders.price + 0.1, 530 | amended_qty=TestBasicOrders.lot_size * 4, 531 | ) 532 | 533 | self.assertEqual(3, len(trades8)) 534 | self.check_order( 535 | order=order8, order_id=8, instmt=TestBasicOrders.instmt, 536 | price=TestBasicOrders.price + 0.1, 537 | qty=TestBasicOrders.lot_size * 4, 538 | side=lme.Side.SELL, cum_qty=TestBasicOrders.lot_size * 4, 539 | leaves_qty=0.0 540 | ) 541 | self.check_trade( 542 | trade=trades8[0], order_id=8, instmt=TestBasicOrders.instmt, 543 | trade_price=TestBasicOrders.price + 0.1, 544 | trade_qty=TestBasicOrders.lot_size * 4, trade_side=lme.Side.SELL, 545 | trade_id=3 546 | ) 547 | self.check_trade( 548 | trade=trades8[1], order_id=6, instmt=TestBasicOrders.instmt, 549 | trade_price=TestBasicOrders.price + 0.1, 550 | trade_qty=TestBasicOrders.lot_size, trade_side=lme.Side.BUY, 551 | trade_id=4 552 | ) 553 | self.check_trade( 554 | trade=trades8[2], order_id=7, instmt=TestBasicOrders.instmt, 555 | trade_price=TestBasicOrders.price + 0.1, 556 | trade_qty=TestBasicOrders.lot_size * 3, trade_side=lme.Side.BUY, 557 | trade_id=5 558 | ) 559 | 560 | 561 | if __name__ == '__main__': 562 | unittest.main() 563 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,py37 3 | 4 | [testenv] 5 | commands = py.test tests 6 | deps = pytest 7 | --------------------------------------------------------------------------------