diff --git a/.travis.yml b/.travis.yml index 52515c2..b72ce43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,47 @@ language: python -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - 3.7 - - pypy +# python: +# - 2.7 +# - 3.4 +# - 3.5 +# - 3.6 +# - 3.7 +# - pypy +matrix: + include: + - os: linux + dist: xenial + env: PYTHON=3.6 CPP=17 GCC=7 + name: Python 3.6, c++17, gcc 7 + addons: + apt: + sources: + - deadsnakes + - ubuntu-toolchain-r-test + packages: + - g++-7 + - python3.6-dev + - python3.6-venv -install: - - pip install .[performance] +before_install: + - | + virtualenv -p python3.6 env + source env/bin/activate + python -m pip install -U pip setuptools wheel +install: + - | + python -m pip install 'pybind11>=2.3' + python -m pip install .[performance] + python -m pip install pytest + script: - - make test + - pip freeze + - which python + - ls -lah /home/travis/build/gavincyi/LightMatchingEngine/env/lib/python3.6 + - ls -lah /home/travis/build/gavincyi/LightMatchingEngine/env/lib/python3.6/site-packages + - ls -lah /home/travis/build/gavincyi/LightMatchingEngine/env/lib/python3.6/site-packages/lightmatchingengine + - python -c "import lightmatchingengine" + - python -c "import lightmatchingengine.lightmatchingengine" + - pytest tests/unit - python tests/performance/performance_test.py --freq 20 --num-orders 500 diff --git a/lightmatchingengine/__init__.py b/lightmatchingengine/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lightmatchingengine/lightmatchingengine.pyx b/lightmatchingengine/lightmatchingengine.pyx deleted file mode 100644 index 61307a5..0000000 --- a/lightmatchingengine/lightmatchingengine.pyx +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/python3 -cpdef enum Side: - BUY = 1 - SELL = 2 - - -cdef class Order: - cdef public int order_id - cdef public str instmt - cdef public double price - cdef public double qty - cdef public double cum_qty - cdef public double leaves_qty - cdef public Side side - - def __init__(self, order_id, instmt, price, qty, side): - """ - Constructor - """ - self.order_id = order_id - self.instmt = instmt - self.price = price - self.qty = qty - self.cum_qty = 0 - self.leaves_qty = qty - self.side = side - - -cdef class OrderBook: - cdef public dict bids - cdef public dict asks - cdef public dict order_id_map - - def __init__(self): - """ - Constructor - """ - self.bids = {} - self.asks = {} - self.order_id_map = {} - - -cdef class Trade: - cdef public int order_id - cdef public str instmt - cdef public double trade_price - cdef public double trade_qty - cdef public Side trade_side - cdef public int trade_id - - def __init__(self, order_id, instmt, trade_price, trade_qty, trade_side, trade_id): - """ - Constructor - """ - self.order_id = order_id - self.instmt = instmt - self.trade_price = trade_price - self.trade_qty = trade_qty - self.trade_side = trade_side - self.trade_id = trade_id - - -cdef class LightMatchingEngine: - cdef public dict order_books - cdef public int curr_order_id - cdef public int curr_trade_id - - def __init__(self): - """ - Constructor - """ - self.order_books = {} - self.curr_order_id = 0 - self.curr_trade_id = 0 - - cpdef add_order(self, str instmt, double price, double qty, Side side): - """ - Add an order - :param instmt Instrument name - :param price Price, defined as zero if market order - :param qty Order quantity - :param side 1 for BUY, 2 for SELL. Defaulted as BUY. - :return The order and the list of trades. - Empty list if there is no matching. - """ - cdef list trades = [] - cdef int order_id - cdef Order order - - assert side == Side.BUY or side == Side.SELL, \ - "Invalid side %s" % side - - # Locate the order book - order_book = self.order_books.setdefault(instmt, OrderBook()) - - # Initialization - self.curr_order_id += 1 - order_id = self.curr_order_id - order = Order(order_id, instmt, price, qty, side) - - if side == Side.BUY: - # Buy - best_price = min(order_book.asks.keys()) if len(order_book.asks) > 0 \ - else None - while best_price is not None and \ - (price == 0.0 or price >= best_price ) and \ - order.leaves_qty >= 1e-9: - best_price_qty = sum([ask.leaves_qty for ask in order_book.asks[best_price]]) - match_qty = min(best_price_qty, order.leaves_qty) - assert match_qty > 0, "Match quantity must be larger than zero" - - # Generate aggressive order trade first - self.curr_trade_id += 1 - order.cum_qty += match_qty - order.leaves_qty -= match_qty - trades.append(Trade(order_id, instmt, best_price, match_qty, \ - Side.BUY, self.curr_trade_id)) - - # Generate the passive executions - while match_qty >= 1e-9: - # The order hit - hit_order = order_book.asks[best_price][0] - # The order quantity hit - order_match_qty = min(match_qty, hit_order.leaves_qty) - self.curr_trade_id += 1 - trades.append(Trade(hit_order.order_id, instmt, best_price, \ - order_match_qty, \ - Side.SELL, self.curr_trade_id)) - hit_order.cum_qty += order_match_qty - hit_order.leaves_qty -= order_match_qty - match_qty -= order_match_qty - if hit_order.leaves_qty < 1e-9: - del order_book.asks[best_price][0] - - # If the price does not have orders, delete the particular price depth - if len(order_book.asks[best_price]) == 0: - del order_book.asks[best_price] - - # Update the best price - best_price = min(order_book.asks.keys()) if len(order_book.asks) > 0 \ - else None - - # Add the remaining order into the depth - if order.leaves_qty > 0.0: - depth = order_book.bids.setdefault(price, []) - depth.append(order) - order_book.order_id_map[order_id] = order - else: - #Sell - best_price = max(order_book.bids.keys()) if len(order_book.bids) > 0 \ - else None - while best_price is not None and \ - (price == 0.0 or price <= best_price) and \ - order.leaves_qty >= 1e-9: - best_price_qty = sum([bid.leaves_qty for bid in order_book.bids[best_price]]) - match_qty = min(best_price_qty, order.leaves_qty) - assert match_qty >= 1e-9, "Match quantity must be larger than zero" - - # Generate aggressive order trade first - self.curr_trade_id += 1 - order.cum_qty += match_qty - order.leaves_qty -= match_qty - trades.append(Trade(order_id, instmt, best_price, match_qty, \ - Side.SELL, self.curr_trade_id)) - - # Generate the passive executions - while match_qty >= 1e-9: - # The order hit - hit_order = order_book.bids[best_price][0] - # The order quantity hit - order_match_qty = min(match_qty, hit_order.leaves_qty) - self.curr_trade_id += 1 - trades.append(Trade(hit_order.order_id, instmt, best_price, \ - order_match_qty, \ - Side.BUY, self.curr_trade_id)) - hit_order.cum_qty += order_match_qty - hit_order.leaves_qty -= order_match_qty - match_qty -= order_match_qty - if hit_order.leaves_qty < 1e-9: - del order_book.bids[best_price][0] - - # If the price does not have orders, delete the particular price depth - if len(order_book.bids[best_price]) == 0: - del order_book.bids[best_price] - - # Update the best price - best_price = max(order_book.bids.keys()) if len(order_book.bids) > 0 \ - else None - - # Add the remaining order into the depth - if order.leaves_qty >= 1e-9: - depth = order_book.asks.setdefault(price, []) - depth.append(order) - order_book.order_id_map[order_id] = order - - return order, trades - - cpdef cancel_order(self, int order_id, str instmt): - """ - Cancel order - :param order_id Order ID - :param instmt Instrument - :return The order if the cancellation is successful - """ - cdef Order order - cdef double order_price - cdef Side side - cdef int index - - assert instmt in self.order_books.keys(), \ - "Instrument %s is not valid in the order book" % instmt - order_book = self.order_books[instmt] - - if order_id not in order_book.order_id_map.keys(): - # Invalid order id - return None - - order = order_book.order_id_map[order_id] - order_price = order.price - order_id = order.order_id - side = order.side - - if side == Side.BUY: - assert order_price in order_book.bids.keys(), \ - "Order price %.6f is not in the bid price depth" % order_price - price_level = order_book.bids[order_price] - else: - assert order_price in order_book.asks.keys(), \ - "Order price %.6f is not in the ask price depth" % order_price - price_level = order_book.asks[order_price] - - index = 0 - price_level_len = len(price_level) - while index < price_level_len: - if price_level[index].order_id == order_id: - del price_level[index] - break - index += 1 - - if index == price_level_len: - # Cannot find the order ID. Incorrect side - return None - - if side == Side.BUY and len(order_book.bids[order_price]) == 0: - # Delete empty particular price level - del order_book.bids[order_price] - elif side == Side.SELL and len(order_book.asks[order_price]) == 0: - # Delete empty particular price level - del order_book.asks[order_price] - - # Delete the order id from the map - del order_book.order_id_map[order_id] - - # Zero out leaves qty - order.leaves_qty = 0 - - return order diff --git a/setup.py b/setup.py index 5c736cb..065751c 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,97 @@ from setuptools import setup, find_packages, Extension +from setuptools.command.build_ext import build_ext +import sys +import setuptools + +class get_pybind_include(object): + """Helper class to determine the pybind11 include path + The purpose of this class is to postpone importing pybind11 + until it is actually installed, so that the ``get_include()`` + method can be invoked. """ + + def __init__(self, user=False): + self.user = user + + def __str__(self): + import pybind11 + return pybind11.get_include(self.user) + + +ext_modules = [ + Extension( + 'lightmatchingengine.lightmatchingengine', + ['src/lightmatchingengine.cpp'], + include_dirs=[ + # Path to pybind11 headers + get_pybind_include(), + get_pybind_include(user=True) + ], + language='c++' + ), +] + + +# As of Python 3.6, CCompiler has a `has_flag` method. +# cf http://bugs.python.org/issue26689 +def has_flag(compiler, flagname): + """Return a boolean indicating whether a flag name is supported on + the specified compiler. + """ + import tempfile + with tempfile.NamedTemporaryFile('w', suffix='.cpp') as f: + f.write('int main (int argc, char **argv) { return 0; }') + try: + compiler.compile([f.name], extra_postargs=[flagname]) + except setuptools.distutils.errors.CompileError: + return False + return True + + +def cpp_flag(compiler): + """Return the -std=c++[11/14/17] compiler flag. + The newer version is prefered over c++11 (when it is available). + """ + flags = ['-std=c++17', '-std=c++14', '-std=c++11'] + + for flag in flags: + if has_flag(compiler, flag): return flag + + raise RuntimeError('Unsupported compiler -- at least C++11 support ' + 'is needed!') + + +class BuildExt(build_ext): + """A custom build extension for adding compiler-specific options.""" + c_opts = { + 'msvc': ['/EHsc'], + 'unix': [], + } + l_opts = { + 'msvc': [], + 'unix': [], + } + + if sys.platform == 'darwin': + darwin_opts = ['-stdlib=libc++', '-mmacosx-version-min=10.7'] + c_opts['unix'] += darwin_opts + l_opts['unix'] += darwin_opts + + def build_extensions(self): + ct = self.compiler.compiler_type + opts = self.c_opts.get(ct, []) + link_opts = self.l_opts.get(ct, []) + if ct == 'unix': + opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version()) + opts.append(cpp_flag(self.compiler)) + if has_flag(self.compiler, '-fvisibility=hidden'): + opts.append('-fvisibility=hidden') + elif ct == 'msvc': + opts.append('/DVERSION_INFO=\\"%s\\"' % self.distribution.get_version()) + for ext in self.extensions: + ext.extra_compile_args = opts + ext.extra_link_args = link_opts + build_ext.build_extensions(self) + setup( name="lightmatchingengine", @@ -12,12 +105,13 @@ packages=find_packages(exclude=('tests',)), - use_scm_version=True, - install_requires=[], - setup_requires=['setuptools_scm', 'cython'], - ext_modules=[Extension( - 'lightmatchingengine.lightmatchingengine', - ['lightmatchingengine/lightmatchingengine.pyx'])], + # use_scm_version=True, + version='2019.2', + install_requires=['pybind11'], + # setup_requires=['setuptools_scm', 'cython'], + setup_requires=['setuptools_scm', 'pybind11'], + ext_modules=ext_modules, + cmdclass={'build_ext': BuildExt}, tests_require=[ 'pytest' ], diff --git a/src/lightmatchingengine.cpp b/src/lightmatchingengine.cpp new file mode 100644 index 0000000..0db255d --- /dev/null +++ b/src/lightmatchingengine.cpp @@ -0,0 +1,74 @@ +#include +#include +#include "lightmatchingengine.h" + +namespace py = pybind11; + +PYBIND11_MODULE(lightmatchingengine, m) { + // Enum Side + py::enum_(m, "Side") + .value("BUY", Side::BUY) + .value("SELL", Side::SELL) + .export_values(); + + // Struct Order + py::class_(m, "Order") + .def_readwrite("order_id", &Order::order_id) + .def_readwrite("instmt", &Order::instmt) + .def_readwrite("price", &Order::price) + .def_readwrite("qty", &Order::qty) + .def_readwrite("cum_qty", &Order::cum_qty) + .def_readwrite("leaves_qty", &Order::leaves_qty) + .def_readwrite("side", &Order::side) + .def(py::init< + int, + string&, + double, + double, + Side>()) + .def("__str__", &Order::to_string) + .def("__repr__", &Order::to_string); + + // Struct Trade + py::class_(m, "Trade") + .def_readwrite("order_id", &Trade::order_id) + .def_readwrite("instmt", &Trade::instmt) + .def_readwrite("trade_price", &Trade::trade_price) + .def_readwrite("trade_qty", &Trade::trade_qty) + .def_readwrite("trade_side", &Trade::trade_side) + .def_readwrite("trade_id", &Trade::trade_id) + .def(py::init< + int, + const string&, + double, + double, + Side, + int>()) + .def("__str__", &Trade::to_string) + .def("__repr__", &Trade::to_string); + + // Late binding + // py::bind_vector>(m, "DequeOrder"); + py::bind_map>>(m, "MapDoubleVectorOrder"); + py::bind_map>(m, "UnorderedMapIntOrder"); + + // Class OrderBook + py::class_(m, "OrderBook") + .def(py::init()) + .def_readwrite("bids", &OrderBook::bids) + .def_readwrite("asks", &OrderBook::asks) + .def_readwrite("order_id_map", &OrderBook::order_id_map); + + + // Class LightMatchingEngine + py::bind_map>(m, "UnorderedMapStringOrderBook"); + py::class_(m, "LightMatchingEngine") + .def(py::init()) + .def("add_order", &LightMatchingEngine::add_order, py::return_value_policy::reference_internal) + .def("cancel_order", &LightMatchingEngine::cancel_order, py::return_value_policy::reference_internal) + .def("get_bid_queue", &LightMatchingEngine::get_bid_queue, py::return_value_policy::reference) + .def("get_ask_queue", &LightMatchingEngine::get_ask_queue, py::return_value_policy::reference) + .def_property_readonly("order_books", &LightMatchingEngine::order_books, py::return_value_policy::reference) + .def_property_readonly("curr_order_id", &LightMatchingEngine::curr_order_id) + .def_property_readonly("curr_trade_id", &LightMatchingEngine::curr_trade_id); +} diff --git a/src/lightmatchingengine.h b/src/lightmatchingengine.h new file mode 100644 index 0000000..882ea4c --- /dev/null +++ b/src/lightmatchingengine.h @@ -0,0 +1,388 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +namespace py = pybind11; + +#define MIN_NPRICE LLONG_MIN +#define MAX_NPRICE LLONG_MAX +#define MIN_QUANTITY 1e-9 +#define MIN_TICK_SIZE 1e-9 +#define EPILSON 5e-10 +#define nprice_t long long +#define price_t double +#define qty_t double +#define id_t long long +#define NORMALIZE_PRICE( x ) static_cast( x / MIN_TICK_SIZE + EPILSON ) +#define DENORMALIZE_PRICE( x ) ( static_cast(x) * MIN_TICK_SIZE ) + +enum class Side { + BUY = 1, + SELL = 2 +}; + +struct Order { + id_t order_id; + string instmt; + price_t price; + qty_t qty; + qty_t cum_qty; + qty_t leaves_qty; + Side side; + + Order(id_t order_id, const string& instmt, price_t price, qty_t qty, Side side): + order_id(order_id), + instmt(instmt), + price(price), + qty(qty), + cum_qty(0), + leaves_qty(qty), + side(side) {} + + string to_string() { + ostringstream sstream; + sstream << "Order Id: " << order_id << ", " + << "instmt: " << instmt << ", " + << "price: " << price << ", " + << "qty: " << qty << ", " + << "cum_qty: " << cum_qty << ", " + << "leaves_qty: " << leaves_qty << ", " + << "side: " << (side == Side::BUY ? "Buy" : "Sell"); + return sstream.str(); + } +}; + +struct Trade { + id_t order_id; + string instmt; + price_t trade_price; + qty_t trade_qty; + Side trade_side; + id_t trade_id; + + Trade(id_t order_id, const string& instmt, price_t trade_price, qty_t trade_qty, + Side trade_side, id_t trade_id): + order_id(order_id), + instmt(instmt), + trade_price(trade_price), + trade_qty(trade_qty), + trade_side(trade_side), + trade_id(trade_id) {} + + string to_string() { + ostringstream sstream; + sstream << "Trade Id: " << trade_id << ", " + << "order Id: " << order_id << ", " + << "instmt: " << instmt << ", " + << "trade_price: " << trade_price << ", " + << "trade_qty: " << trade_qty << ", " + << "trade_side: " << (trade_side == Side::BUY ? "Buy" : "Sell"); + return sstream.str(); + } +}; + +PYBIND11_MAKE_OPAQUE(map>); +PYBIND11_MAKE_OPAQUE(unordered_map); + +struct OrderBook { + map> bids; + map> asks; + unordered_map order_id_map; + + OrderBook() = default; + /* OrderBook(const OrderBook&) = delete; */ + OrderBook(OrderBook&&) = default; + /* OrderBook& operator=(OrderBook&&) = default; */ + OrderBook& operator=(const OrderBook&) = delete; +}; + +PYBIND11_MAKE_OPAQUE(unordered_map); + +class LightMatchingEngine { + public: + unordered_map& order_books() { + return __order_books; + } + + int curr_order_id() { + return __curr_order_id; + } + + int curr_trade_id() { + return __curr_trade_id; + } + + deque* get_bid_queue(const string& instmt, price_t price) { + auto& order_book = *__get_order_book(instmt); + auto nprice = NORMALIZE_PRICE(price); + auto order_queue_it = order_book.bids.find(nprice); + if (order_queue_it == order_book.bids.end()) { + ostringstream sstream; + sstream << "Order price " << price << " cannot be found " + << "in the " << instmt << " bid order book"; + throw runtime_error(sstream.str()); + } + + return &(order_queue_it->second); + } + + deque* get_ask_queue(const string& instmt, price_t price) { + auto& order_book = *__get_order_book(instmt); + auto nprice = NORMALIZE_PRICE(price); + auto order_queue_it = order_book.asks.find(nprice); + if (order_queue_it == order_book.asks.end()) { + ostringstream sstream; + sstream << "Order price " << price << " cannot be found " + << "in the " << instmt << " bid order book"; + throw runtime_error(sstream.str()); + } + + return &(order_queue_it->second); + } + + tuple> add_order( + const string& instmt, price_t price, qty_t qty, Side side) + { + vector trades; + id_t order_id = (__curr_order_id += 1); + Order* order_ptr = new Order(order_id, instmt, price, qty, side); + auto& order = *order_ptr; + nprice_t nprice = NORMALIZE_PRICE(price); + + // Find the order book + auto order_book_it = __order_books.find(instmt); + if (order_book_it == __order_books.end()) { + order_book_it = __order_books.emplace(instmt, OrderBook()).first; + } + + auto& order_book = order_book_it->second; + + if (side == Side::BUY) { + nprice_t best_nprice = MAX_NPRICE; + if (order_book.asks.size() > 0) { + best_nprice = order_book.asks.begin()->first; + } + + while (nprice >= best_nprice && order.leaves_qty > MIN_QUANTITY) { + auto& nbbo = order_book.asks.begin()->second; + auto original_leaves_qty = order.leaves_qty; + + // Matching the ask queue + while (nbbo.size() > 0 && order.leaves_qty > MIN_QUANTITY) { + auto& front_nbbo = *(nbbo.front()); + qty_t matching_qty = min(order.leaves_qty, front_nbbo.leaves_qty); + assert(matching_qty >= MIN_QUANTITY); + order.cum_qty += matching_qty; + order.leaves_qty -= matching_qty; + front_nbbo.cum_qty += matching_qty; + front_nbbo.leaves_qty -= matching_qty; + + // Trades on the passive order + trades.push_back(Trade( + front_nbbo.order_id, + instmt, + DENORMALIZE_PRICE(best_nprice), + matching_qty, + front_nbbo.side, + ++__curr_trade_id)); + + // Remove the order if it is fully executed + if (front_nbbo.leaves_qty < MIN_QUANTITY) { + order_book.order_id_map.erase(front_nbbo.order_id); + nbbo.pop_front(); + } + } + + // Trades from the original order + trades.push_back(Trade( + order.order_id, + instmt, + DENORMALIZE_PRICE(best_nprice), + original_leaves_qty - order.leaves_qty, + order.side, + ++__curr_trade_id)); + + // Remove the ask queue if the size = 0 + if (nbbo.size() == 0) { + order_book.asks.erase(order_book.asks.begin()); + } + + // Update the ask best prices + if (order_book.asks.size() > 0) { + best_nprice = order_book.asks.begin()->first; + } else { + best_nprice = MAX_NPRICE; + } + } + + // After matching the order, place the leaving order to the end + // of the order book queue, and create the order id mapping + if (order.leaves_qty > MIN_QUANTITY) { + auto nbbo_it = order_book.bids.find(nprice); + if (nbbo_it == order_book.bids.end()){ + nbbo_it = order_book.bids.emplace(nprice, deque()).first; + } + + auto& nbbo = nbbo_it->second; + nbbo.emplace_back(&order); + order_book.order_id_map.emplace(order.order_id, &order); + } + } else { + nprice_t best_nprice = MIN_NPRICE; + if (order_book.bids.size() > 0) { + best_nprice = order_book.bids.rbegin()->first; + } + + while (nprice <= best_nprice && order.leaves_qty > MIN_QUANTITY) { + auto& nbbo = order_book.bids.rbegin()->second; + auto original_leaves_qty = order.leaves_qty; + + // Matching the ask queue + while (nbbo.size() > 0 && order.leaves_qty > MIN_QUANTITY) { + auto& front_nbbo = *(nbbo.front()); + qty_t matching_qty = min(order.leaves_qty, front_nbbo.leaves_qty); + assert(matching_qty >= MIN_QUANTITY); + order.cum_qty += matching_qty; + order.leaves_qty -= matching_qty; + front_nbbo.cum_qty += matching_qty; + front_nbbo.leaves_qty -= matching_qty; + + // Trades on the passive order + trades.push_back(Trade( + front_nbbo.order_id, + instmt, + DENORMALIZE_PRICE(best_nprice), + matching_qty, + front_nbbo.side, + ++__curr_trade_id)); + + // Remove the order if it is fully executed + if (front_nbbo.leaves_qty < MIN_QUANTITY) { + order_book.order_id_map.erase(front_nbbo.order_id); + nbbo.pop_front(); + } + } + + // Trades from the original order + trades.push_back(Trade( + order.order_id, + instmt, + DENORMALIZE_PRICE(best_nprice), + original_leaves_qty - order.leaves_qty, + order.side, + ++__curr_trade_id)); + + // Remove the bid queue if the size = 0 + if (nbbo.size() == 0) { + order_book.bids.erase(next(order_book.bids.rbegin()).base()); + } + + // Update the bid best prices + if (order_book.bids.size() > 0) { + best_nprice = order_book.bids.rbegin()->first; + } else { + best_nprice = MIN_NPRICE; + } + } + + // After matching the order, place the leaving order to the end + // of the order book queue, and create the order id mapping + if (order.leaves_qty > MIN_QUANTITY) { + auto nbbo_it = order_book.asks.find(nprice); + if (nbbo_it == order_book.asks.end()){ + nbbo_it = order_book.asks.emplace(nprice, deque()).first; + } + + auto& nbbo = nbbo_it->second; + nbbo.emplace_back(&order); + order_book.order_id_map.emplace(order.order_id, &order); + } + } + + return make_tuple(&order, trades); + } + + Order* cancel_order(id_t order_id, const string& instmt) { + auto order_book_it = __order_books.find(instmt); + if (order_book_it == __order_books.end()) { + ostringstream sstream; + sstream << "Order books do not have the instrument " << instmt; + throw runtime_error(sstream.str()); + } + + auto& order_book = order_book_it->second; + auto order_it = order_book.order_id_map.find(order_id); + if (order_it == order_book.order_id_map.end()) { + ostringstream sstream; + sstream << "Cannot find order " << order_id << " from instrument " << instmt; + throw runtime_error(sstream.str()); + } + + auto& order = *order_it->second; + auto nprice = NORMALIZE_PRICE(order.price); + + if (order.side == Side::BUY) { + auto order_queue_it = order_book.bids.find(nprice); + assert(order_queue_it != order_book.bids.end()); + auto& order_queue = order_queue_it->second; + auto found_order = find_if( + order_queue.begin(), order_queue.end(), + [&order](auto o) { return o->order_id == order.order_id; }); + + assert(found_order != order_queue.end()); + + // Remove the order from the matching engine + order_queue.erase(found_order); + + // Remove the nbbo if no queue on it + if (order_queue.size() == 0) { + order_book.bids.erase(nprice); + } + } else { + auto order_queue_it = order_book.asks.find(nprice); + assert(order_queue_it != order_book.asks.end()); + auto& order_queue = order_queue_it->second; + auto found_order = find_if( + order_queue.begin(), order_queue.end(), + [&order](auto o) { return o->order_id == order.order_id; }); + + assert(found_order != order_queue.end()); + + // Remove the order from the matching engine + order_queue.erase(found_order); + + // Remove the nbbo if no queue on it + if (order_queue.size() == 0) { + order_book.asks.erase(nprice); + } + } + + // Finally set the leaves qty to 0 + order.leaves_qty = 0.0; + order_book.order_id_map.erase(order_it); + + return ℴ + } + + private: + unordered_map __order_books; + int __curr_order_id; + int __curr_trade_id; + + OrderBook* __get_order_book(const string& instmt) { + auto order_book_it = __order_books.find(instmt); + if (order_book_it == __order_books.end()) { + auto err_message = string("Order books do not have the instrument ") + instmt; + throw runtime_error(err_message); + } + + return &(order_book_it->second); + } +}; diff --git a/tests/performance/performance_test.py b/tests/performance/performance_test.py index 8fe5a50..0a18e66 100644 --- a/tests/performance/performance_test.py +++ b/tests/performance/performance_test.py @@ -15,6 +15,7 @@ --tick-size= Tick size. [Default: 0.1] --gamma-quantity= Gamma value in the gamma distribution for the order quantity. [Default: 2] + --debug Debug mode. """ from docopt import docopt import logging @@ -67,16 +68,21 @@ def run(args): if uniform(0, 1) <= add_order_prob or len(orders) == 0: price = np.random.standard_normal() * std_price + mean_price price = int(price / tick_size) * tick_size - quantity = np.random.gamma(gamma_quantity) + 1 + quantity = int(np.random.gamma(gamma_quantity)) + 1 side = Side.BUY if uniform(0, 1) <= 0.5 else Side.SELL + LOGGER.debug('Adding order at side %s, price %s ' + 'and quantity %s', + side, price, quantity) + # Add the order with Timer() as timer: order, trades = engine.add_order(symbol, price, quantity, side) LOGGER.debug('Order %s is added at side %s, price %s ' - 'and quantity %s', - order.order_id, order.side, order.price, order.qty) + 'and quantity %s, with %s trades', + order.order_id, order.side, order.price, order.qty, + len(trades)) # Save the order if there is any quantity left if order.leaves_qty > 0.0: @@ -84,6 +90,8 @@ def run(args): # Remove the trades for trade in trades: + LOGGER.debug('Trade of order id %s is detected with trade ' + 'qty %s', trade.order_id, trade.trade_qty) if (trade.order_id != order.order_id and orders[trade.order_id].leaves_qty < 1e-9): del orders[trade.order_id] @@ -100,10 +108,13 @@ def run(args): order_id = list(orders.keys())[index] + LOGGER.debug('Deleting order %s', order_id) + with Timer() as timer: engine.cancel_order(order_id, order.instmt) LOGGER.debug('Order %s is deleted', order_id) + del orders[order_id] # Save the statistics @@ -153,7 +164,10 @@ def describe_statistics(add_statistics, cancel_statistics): if __name__ == '__main__': args = docopt(__doc__, version='1.0.0') - logging.basicConfig(level=logging.INFO) + if args['--debug']: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) LOGGER.info('Running the performance benchmark') add_statistics, cancel_statistics = run(args) diff --git a/tests/unit/test_basic_orders.py b/tests/unit/test_basic_orders.py index 6a4e831..32a6a0b 100644 --- a/tests/unit/test_basic_orders.py +++ b/tests/unit/test_basic_orders.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/python3 import lightmatchingengine.lightmatchingengine as lme import unittest @@ -6,7 +6,7 @@ class TestBasicOrders(unittest.TestCase): instmt = "TestingInstrument" price = 100.0 lot_size = 1.0 - + def check_order(self, order, order_id, instmt, price, qty, side, cum_qty, leaves_qty): """ Check the order information @@ -19,11 +19,11 @@ def check_order(self, order, order_id, instmt, price, qty, side, cum_qty, leaves self.assertEqual(side, order.side) self.assertEqual(cum_qty, order.cum_qty) self.assertEqual(leaves_qty, order.leaves_qty) - + def check_trade(self, trade, order_id, instmt, trade_price, trade_qty, trade_side, trade_id): """ Check the trade information - """ + """ self.assertTrue(trade is not None) self.assertEqual(order_id, trade.order_id) self.assertEqual(instmt, trade.instmt) @@ -31,15 +31,15 @@ def check_trade(self, trade, order_id, instmt, trade_price, trade_qty, trade_sid self.assertEqual(trade_qty, trade.trade_qty) self.assertEqual(trade_side, trade.trade_side) self.assertEqual(trade_id, trade.trade_id) - + def check_order_book(self, me, instmt, num_bids_level, num_asks_level): """ Check the order book depth """ - self.assertTrue(instmt in me.order_books.keys()) + self.assertTrue(instmt in me.order_books) self.assertEqual(num_bids_level, len(me.order_books[instmt].bids)) - self.assertEqual(num_asks_level, len(me.order_books[instmt].asks)) - + self.assertEqual(num_asks_level, len(me.order_books[instmt].asks)) + def check_deleted_order(self, order, del_order): """ Check if the deleted order is same as the original order @@ -48,10 +48,10 @@ def check_deleted_order(self, order, del_order): self.assertTrue(del_order is not None) self.assertEqual(order, del_order) self.assertEqual(0, del_order.leaves_qty) - + def test_cancel_order(self): me = lme.LightMatchingEngine() - + # Place a buy order order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ @@ -61,13 +61,13 @@ def test_cancel_order(self): self.check_order_book(me, TestBasicOrders.instmt, 1, 0) self.check_order(order, 1, TestBasicOrders.instmt, TestBasicOrders.price, \ TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + # Cancel a buy order del_order = me.cancel_order(order.order_id, TestBasicOrders.instmt) self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) self.check_deleted_order(order, del_order) - + # Place a sell order order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ @@ -76,16 +76,16 @@ def test_cancel_order(self): self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, 0, 1) self.check_order(order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ - TestBasicOrders.lot_size, lme.Side.SELL, 0, TestBasicOrders.lot_size) - + TestBasicOrders.lot_size, lme.Side.SELL, 0, TestBasicOrders.lot_size) + # Cancel a sell order del_order = me.cancel_order(order.order_id, TestBasicOrders.instmt) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) self.check_deleted_order(order, del_order) - + def test_fill_order(self): me = lme.LightMatchingEngine() - + # Place a buy order buy_order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ @@ -95,28 +95,28 @@ def test_fill_order(self): self.check_order_book(me, TestBasicOrders.instmt, 1, 0) self.check_order(buy_order, 1, TestBasicOrders.instmt, TestBasicOrders.price, \ TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + # Place a sell order sell_order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ TestBasicOrders.lot_size, \ - lme.Side.SELL) + lme.Side.SELL) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) - self.assertEqual(2, len(trades)) + self.assertEqual(2, len(trades)) self.check_order(buy_order, 1, TestBasicOrders.instmt, TestBasicOrders.price, \ TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) self.check_order(sell_order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ TestBasicOrders.lot_size, lme.Side.SELL, TestBasicOrders.lot_size, 0) - + # Check trades - self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ - sell_order.price, sell_order.qty, sell_order.side, 1) - self.check_trade(trades[1], buy_order.order_id, buy_order.instmt, \ - buy_order.price, buy_order.qty, buy_order.side, 2) - + self.check_trade(trades[0], buy_order.order_id, buy_order.instmt, \ + buy_order.price, buy_order.qty, buy_order.side, 1) + self.check_trade(trades[1], sell_order.order_id, sell_order.instmt, \ + sell_order.price, sell_order.qty, sell_order.side, 2) + def test_fill_multiple_orders_same_level(self): me = lme.LightMatchingEngine() - + # Place buy orders for i in range(1, 11): buy_order, trades = me.add_order(TestBasicOrders.instmt, \ @@ -125,34 +125,34 @@ def test_fill_multiple_orders_same_level(self): lme.Side.BUY) self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, 1, 0) - self.assertEqual(i, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price])) + self.assertEqual(i, len(me.get_bid_queue(TestBasicOrders.instmt, TestBasicOrders.price))) self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price, \ - TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) + # Place sell orders sell_order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ 10.0 * TestBasicOrders.lot_size, \ - lme.Side.SELL) + lme.Side.SELL) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) - self.assertEqual(11, len(trades)) + self.assertEqual(11, len(trades)) self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price, \ - TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) + TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) self.check_order(sell_order, 11, TestBasicOrders.instmt, TestBasicOrders.price, \ - 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) - - # Check aggressive hit orders - self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ - sell_order.price, sell_order.qty, sell_order.side, 1) - + 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) + # Check passive hit orders for i in range(1, 11): - self.check_trade(trades[i], i, buy_order.instmt, \ - buy_order.price, buy_order.qty, buy_order.side, i+1) - + self.check_trade(trades[i-1], i, buy_order.instmt, \ + buy_order.price, buy_order.qty, buy_order.side, i) + + # Check aggressive hit orders + self.check_trade(trades[10], sell_order.order_id, sell_order.instmt, \ + sell_order.price, sell_order.qty, sell_order.side, 11) + def test_fill_multiple_orders_different_level(self): me = lme.LightMatchingEngine() - + # Place buy orders for i in range(1, 11): buy_order, trades = me.add_order(TestBasicOrders.instmt, \ @@ -161,42 +161,42 @@ def test_fill_multiple_orders_different_level(self): lme.Side.BUY) self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, i, 0) - self.assertEqual(1, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price+i])) + self.assertEqual(1, len(me.get_bid_queue(TestBasicOrders.instmt, TestBasicOrders.price+i))) self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price+i, \ - TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) + # Place sell orders sell_order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ 10.0 * TestBasicOrders.lot_size, \ - lme.Side.SELL) + lme.Side.SELL) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) - self.assertEqual(20, len(trades)) + self.assertEqual(20, len(trades)) self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price+10, \ - TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) + TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) self.check_order(sell_order, 11, TestBasicOrders.instmt, TestBasicOrders.price, \ - 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) - + 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) + for i in range(0, 10): match_price = sell_order.price+10-i - self.check_trade(trades[2*i], sell_order.order_id, sell_order.instmt, \ - match_price, TestBasicOrders.lot_size, sell_order.side, 2*i+1) - self.check_trade(trades[2*i+1], 10-i, buy_order.instmt, \ - match_price, TestBasicOrders.lot_size, buy_order.side, 2*i+2) + self.check_trade(trades[2*i], 10-i, buy_order.instmt, \ + match_price, TestBasicOrders.lot_size, buy_order.side, 2*i+1) + self.check_trade(trades[2*i+1], sell_order.order_id, sell_order.instmt, \ + match_price, TestBasicOrders.lot_size, sell_order.side, 2*i+2) def test_cancel_partial_fill_orders(self): me = lme.LightMatchingEngine() - + # Place a buy order buy1_order, trades = me.add_order(TestBasicOrders.instmt, \ - TestBasicOrders.price + 0.1, \ + TestBasicOrders.price + 1, \ TestBasicOrders.lot_size, \ lme.Side.BUY) self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, 1, 0) - self.check_order(buy1_order, 1, TestBasicOrders.instmt, TestBasicOrders.price + 0.1, \ + self.check_order(buy1_order, 1, TestBasicOrders.instmt, TestBasicOrders.price + 1, \ TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + # Place a buy order buy2_order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ @@ -205,40 +205,40 @@ def test_cancel_partial_fill_orders(self): self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, 2, 0) self.check_order(buy2_order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ - 2 * TestBasicOrders.lot_size, lme.Side.BUY, 0, 2 * TestBasicOrders.lot_size) - + 2 * TestBasicOrders.lot_size, lme.Side.BUY, 0, 2 * TestBasicOrders.lot_size) + # Place a sell order sell_order, trades = me.add_order(TestBasicOrders.instmt, \ TestBasicOrders.price, \ 2 * TestBasicOrders.lot_size, \ - lme.Side.SELL) + lme.Side.SELL) self.check_order_book(me, TestBasicOrders.instmt, 1, 0) - self.assertEqual(4, len(trades)) - self.check_order(buy1_order, 1, TestBasicOrders.instmt, TestBasicOrders.price + 0.1, \ + self.assertEqual(4, len(trades)) + self.check_order(buy1_order, 1, TestBasicOrders.instmt, TestBasicOrders.price + 1, \ TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) self.check_order(buy2_order, 2, TestBasicOrders.instmt, TestBasicOrders.price, \ - 2*TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, TestBasicOrders.lot_size) + 2*TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, TestBasicOrders.lot_size) self.check_order(sell_order, 3, TestBasicOrders.instmt, TestBasicOrders.price, \ 2*TestBasicOrders.lot_size, lme.Side.SELL, 2*TestBasicOrders.lot_size, 0) - + # Check trades - self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ - TestBasicOrders.price + 0.1, TestBasicOrders.lot_size, sell_order.side, 1) - self.check_trade(trades[1], buy1_order.order_id, buy1_order.instmt, \ - TestBasicOrders.price + 0.1, TestBasicOrders.lot_size, buy1_order.side, 2) - self.check_trade(trades[2], sell_order.order_id, sell_order.instmt, \ - TestBasicOrders.price, TestBasicOrders.lot_size, sell_order.side, 3) - self.check_trade(trades[3], buy2_order.order_id, buy1_order.instmt, \ - TestBasicOrders.price, TestBasicOrders.lot_size, buy2_order.side, 4) - + self.check_trade(trades[0], buy1_order.order_id, buy1_order.instmt, \ + TestBasicOrders.price + 1, TestBasicOrders.lot_size, buy1_order.side, 1) + self.check_trade(trades[1], sell_order.order_id, sell_order.instmt, \ + TestBasicOrders.price + 1, TestBasicOrders.lot_size, sell_order.side, 2) + self.check_trade(trades[2], buy2_order.order_id, buy1_order.instmt, \ + TestBasicOrders.price, TestBasicOrders.lot_size, buy2_order.side, 3) + self.check_trade(trades[3], sell_order.order_id, sell_order.instmt, \ + TestBasicOrders.price, TestBasicOrders.lot_size, sell_order.side, 4) + # Cancel the second order del_order = me.cancel_order(buy2_order.order_id, TestBasicOrders.instmt) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) self.check_deleted_order(buy2_order, del_order) - + def test_fill_multiple_orders_same_level_market_order(self): me = lme.LightMatchingEngine() - + # Place buy orders for i in range(1, 11): buy_order, trades = me.add_order(TestBasicOrders.instmt, \ @@ -247,34 +247,34 @@ def test_fill_multiple_orders_same_level_market_order(self): lme.Side.BUY) self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, 1, 0) - self.assertEqual(i, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price])) + self.assertEqual(i, len(me.get_bid_queue(TestBasicOrders.instmt, TestBasicOrders.price))) self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price, \ - TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) + # Place sell orders sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 0, \ 10.0 * TestBasicOrders.lot_size, \ - lme.Side.SELL) + lme.Side.SELL) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) - self.assertEqual(11, len(trades)) + self.assertEqual(11, len(trades)) self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price, \ - TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) + TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) self.check_order(sell_order, 11, TestBasicOrders.instmt, 0, \ - 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) - + 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) + # Check aggressive hit orders - Trade price is same as the passive hit limit price - self.check_trade(trades[0], sell_order.order_id, sell_order.instmt, \ - buy_order.price, sell_order.qty, sell_order.side, 1) - + self.check_trade(trades[10], sell_order.order_id, sell_order.instmt, \ + buy_order.price, sell_order.qty, sell_order.side, 11) + # Check passive hit orders for i in range(1, 11): - self.check_trade(trades[i], i, buy_order.instmt, \ - buy_order.price, buy_order.qty, buy_order.side, i+1) - + self.check_trade(trades[i-1], i, buy_order.instmt, \ + buy_order.price, buy_order.qty, buy_order.side, i) + def test_fill_multiple_orders_different_level_market_order(self): me = lme.LightMatchingEngine() - + # Place buy orders for i in range(1, 11): buy_order, trades = me.add_order(TestBasicOrders.instmt, \ @@ -283,28 +283,28 @@ def test_fill_multiple_orders_different_level_market_order(self): lme.Side.BUY) self.assertEqual(0, len(trades)) self.check_order_book(me, TestBasicOrders.instmt, i, 0) - self.assertEqual(1, len(me.order_books[TestBasicOrders.instmt].bids[TestBasicOrders.price+i])) + self.assertEqual(1, len(me.get_bid_queue(TestBasicOrders.instmt, TestBasicOrders.price+i))) self.check_order(buy_order, i, TestBasicOrders.instmt, TestBasicOrders.price+i, \ - TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) - + TestBasicOrders.lot_size, lme.Side.BUY, 0, TestBasicOrders.lot_size) + # Place sell orders sell_order, trades = me.add_order(TestBasicOrders.instmt, \ 0, \ 10.0 * TestBasicOrders.lot_size, \ - lme.Side.SELL) + lme.Side.SELL) self.check_order_book(me, TestBasicOrders.instmt, 0, 0) - self.assertEqual(20, len(trades)) + self.assertEqual(20, len(trades)) self.check_order(buy_order, 10, TestBasicOrders.instmt, TestBasicOrders.price+10, \ - TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) + TestBasicOrders.lot_size, lme.Side.BUY, TestBasicOrders.lot_size, 0) self.check_order(sell_order, 11, TestBasicOrders.instmt, 0, \ - 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) - + 10*TestBasicOrders.lot_size, lme.Side.SELL, 10*TestBasicOrders.lot_size, 0) + for i in range(0, 10): match_price = TestBasicOrders.price+10-i - self.check_trade(trades[2*i], sell_order.order_id, sell_order.instmt, \ - match_price, TestBasicOrders.lot_size, sell_order.side, 2*i+1) - self.check_trade(trades[2*i+1], 10-i, buy_order.instmt, \ - match_price, TestBasicOrders.lot_size, buy_order.side, 2*i+2) - + self.check_trade(trades[2*i+1], sell_order.order_id, sell_order.instmt, \ + match_price, TestBasicOrders.lot_size, sell_order.side, 2*i+2) + self.check_trade(trades[2*i], 10-i, buy_order.instmt, \ + match_price, TestBasicOrders.lot_size, buy_order.side, 2*i+1) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()