def test_aon_incoming(sql_engine: SQLEngine, matching_engine: MatchingEngine): """Test that for an all-or-none incoming order, the policy is properly respected :param sql_engine: :type sql_engine: SQLEngine """ me = matching_engine test_orders = [ Order(order_id=1, security_symbol="X", side="ask", size=100, price=1), Order(order_id=2, security_symbol="X", side="bid", size=120, price=2, all_or_none=True) ] for test_order in test_orders: me.session.add(test_order) me.session.commit() me.heartbeat(incoming=test_order) # Order 1 is first entered into the order book # Order 2 then tries to match with order 1, but since order 1 is less than # order 2, and order 2 has to be fulfilled entirely, no trade can be made # and both orders are entered into the order book assert me.session.query(Transaction).count() == 0 assert me.session.query(Order).count() == 2 # assert me.session.query(Order).get(1) is test_orders[0] # assert me.session.query(Order).get(2) is test_orders[1] assert me.session.query(Order).get(1).active assert me.session.query(Order).get(2).active
def _benchmark( sql_session: Session, queue_conn: pika.BlockingConnection, n_rounds: int) -> ty.Tuple[dt.datetime, ty.List[int], ty.List[float]]: """The core logic of the benchmark: add pseudo users and bench company, inject assets, generate random sizes and prices, create stock order objects, then submit them to the queue :param sql_session: [description] :type sql_session: Session :param queue_conn: [description] :type queue_conn: pika.BlockingConnection :param n_rounds: [description] :type n_rounds: int :return: A start datetime :rtype: dt.datetime """ # Open Message Queue channel ch = queue_conn.channel() ch.queue_declare(queue='incoming_order') # Set up initial data buyer = add_user("buyer", "password", sql_session) seller = add_user("seller", "password", sql_session) bench_company = add_company("BENCH", seller.user_id, sql_session) inject_asset(seller.user_id, "_CASH", 10000000, sql_session) inject_asset(buyer.user_id, "_CASH", 10000000, sql_session) random_sizes = [random.randint(1, 100) for i in range(n_rounds)] random_prices = [random.uniform(10, 100) for i in range(n_rounds)] start_dttm = dt.datetime.utcnow() order_messages = [] for i in range(n_rounds): random_size, random_price = random_sizes[i], random_prices[i] injected_asset = inject_asset(seller.user_id, bench_company.symbol, random_size, sql_session) injected_asset.asset_amount -= random_size sql_session.commit() ask = Order(security_symbol=bench_company.symbol, side="ask", size=random_size, price=random_price, owner_id=seller.user_id) bid = Order(security_symbol=bench_company.symbol, side="bid", size=random_size, price=None, owner_id=buyer.user_id, immediate_or_cancel=True) sql_session.add(ask) sql_session.add(bid) sql_session.commit() order_messages.append(ask.json) order_messages.append(bid.json) for msg in order_messages: ch.basic_publish(exchange='', routing_key='incoming_order', body=msg) return start_dttm, random_sizes, random_prices
def test_simple_static_orders(sql_engine: SQLEngine, matching_engine: MatchingEngine): """Given a fixed set of orders, check that when each of them is entered, the matching occurs correctly :param sql_engine: the SQLAlchemy engine used for connecting to database :type sql_engine: SQLEngine """ me = matching_engine static_orders = [ Order(order_id=0, security_symbol="Y", side="ask", size=100, price=100), Order(order_id=1, security_symbol="X", side="ask", size=100, price=100), Order(order_id=2, security_symbol="X", side="ask", size=100, price=99), Order(order_id=3, security_symbol="X", side="bid", size=120, price=101) ] for static_order in static_orders: me.session.add(static_order) me.session.commit() me.heartbeat(incoming=static_order) # Check at the end of the session that the active orders, the transactions, # and the remaining orders. transaction_1: Transaction = me.session.query(Transaction).get(1) transaction_2: Transaction = me.session.query(Transaction).get(2) assert transaction_1.ask_id == 2 assert transaction_1.bid_id == 3 assert transaction_1.size == 100 assert transaction_1.price == 99 assert transaction_2.ask_id == 1 assert transaction_2.bid_id == 3 assert transaction_2.size == 20 assert transaction_2.price == 100 order_1: Order = me.session.query(Order).get(1) order_2: Order = me.session.query(Order).get(2) order_3: Order = me.session.query(Order).get(3) order_4: Order = me.session.query(Order).get(4) # assert order_1 is static_orders[1] # assert order_2 is static_orders[2] # assert order_3 is static_orders[3] assert order_4.parent_order_id == 1 assert order_4.price == order_1.price assert order_4.size == (order_1.size + order_2.size - order_3.size)
def test_ioc_incoming(sql_engine: SQLEngine, matching_engine: MatchingEngine): """Check that the immediate-or-cancel policy is respected with an incoming order :param sql_engine: [description] :type sql_engine: SQLEngine """ me = matching_engine test_orders = [ Order(order_id=1, security_symbol="X", side="ask", size=100, price=2), Order(order_id=2, security_symbol="X", side="bid", size=120, price=3, immediate_or_cancel=True) ] for test_order in test_orders: me.session.add(test_order) me.session.commit() me.heartbeat(incoming=test_order) # order 1 is first entered into order book # When order 2 comes, it is first matched with order 1 to produce a trade # and a sub-order of size 20 and priced at $3, but given the IOC policy, # this sub-order should be cancelled and not entered into the active orders order_3 = me.session.query(Order).get(3) transaction_1 = me.session.query(Transaction).get(1) assert me.session.query(Order).count() == 3 # assert me.session.query(Order).get(1) is test_orders[0] # assert me.session.query(Order).get(2) is test_orders[1] assert order_3.cancelled_dttm is not None assert order_3.size == 20 assert order_3.price == 3 assert order_3.parent_order_id == 2 # assert len(me.order_book.active_orders) == 0 assert transaction_1.ask_id == 1 assert transaction_1.bid_id == 2 assert transaction_1.price == 2 assert transaction_1.size == 100
def test_market_order(sql_engine: SQLEngine, matching_engine: MatchingEngine): """Check that a market order, which has no price target and is IOC, can trade properly :param sql_engine: [description] :type sql_engine: SQLEngine """ me = matching_engine test_orders = [ Order(order_id=1, security_symbol="X", side="ask", size=100, price=2), Order(order_id=2, security_symbol="X", side="bid", size=120, price=None, immediate_or_cancel=True) ] for test_order in test_orders: me.session.add(test_order) me.session.commit() me.heartbeat(incoming=test_order) # Order 1 is first entered into order book # When order 2 comes, it is first matched with order 1 to produce a trade of # 100 shares at $2, then the remaining 20 shares becomes a sub-order that # is immediately cancelled order_3 = me.session.query(Order).get(3) transaction_1 = me.session.query(Transaction).get(1) assert me.session.query(Order).count() == 3 # assert me.session.query(Order).get(1) is test_orders[0] # assert me.session.query(Order).get(2) is test_orders[1] assert order_3.cancelled_dttm is not None assert order_3.size == 20 assert order_3.price is None assert order_3.parent_order_id == 2 # assert len(me.order_book.active_orders) == 0 assert transaction_1.ask_id == 1 assert transaction_1.bid_id == 2 assert transaction_1.price == 2 assert transaction_1.size == 100
def submit_order(): # Even before rendering the page, try to connect to RabbitMQ. If RabbitMQ # is not running, then redirect to an error page mq = ch = None try: mq: pika.BlockingConnection = get_mq() ch = mq.channel() ch.queue_declare(queue='incoming_order') except AMQPConnectionError as e: error_msg = "Webserver failed to connect to order queue" return redirect(url_for("exchange.error", error_msg=error_msg)) form = OrderSubmitForm(request.form) if request.method == "POST" and form.validate_on_submit(): new_order: Order = Order( security_symbol=form.security_symbol.data, side=form.side.data, size=form.size.data, price=form.price.data, all_or_none=form.all_or_none.data, immediate_or_cancel=form.immediate_or_cancel.data, owner_id=current_user.user_id) if new_order.side == "ask": user_existing_asset = get_db().query(Asset).get( (current_user.user_id, new_order.security_symbol)) if user_existing_asset is None or user_existing_asset.asset_amount < new_order.size: return redirect( url_for("exchange.error", error_msg="Insufficient assets")) else: logger.info( f"Subtracting {new_order.size} shares of {new_order.security_symbol} from {current_user}" ) user_existing_asset.asset_amount -= new_order.size if new_order.price is None: logger.info(f"Marking market order {new_order}") new_order.immediate_or_cancel = True db = get_db() db.add(new_order) db.commit() logger.info(f"{new_order} committed to database") ch.basic_publish(exchange='', routing_key='incoming_order', body=new_order.json) logger.info(f"{new_order} submitted to order queue") return redirect(url_for("exchange.dashboard")) return render_template("exchange/submit_order.html", form=form, title="Submit order")
def test_aon_candidate(sql_engine: SQLEngine, matching_engine: MatchingEngine): """Test that for an all-or-none incoming order, the policy is properly respected :param sql_engine: :type sql_engine: SQLEngine """ me = matching_engine test_orders = [ Order(order_id=1, security_symbol="X", side="ask", size=100, price=2, all_or_none=True), Order(order_id=2, security_symbol="X", side="ask", size=100, price=1), Order(order_id=3, security_symbol="X", side="bid", size=120, price=3) ] for test_order in test_orders: me.session.add(test_order) me.session.commit() me.heartbeat(incoming=test_order) # Order 1 and 2 are first entered into order book # When order 3 comes, it is first matched with order 2 to produce a trade # But the remaining of order 3 does not fulfill order 1 completely, so # no trade is made. The remains of order 3 becomes a suborder with id 4 # and that enters the order book order_4 = me.session.query(Order).get(4) assert me.session.query(Order).count() == 4 # assert me.session.query(Order).get(1) is test_orders[0] # assert me.session.query(Order).get(2) is test_orders[1] # assert me.session.query(Order).get(3) is test_orders[2] assert order_4.size == 20 assert order_4.price == 3 assert order_4.parent_order_id == 3
def msg_callback(ch, method, properties, body): logger.info("Received %r" % body) if not rc['MATCHING_ENGINE_DRY_RUN']: me.heartbeat(Order.from_json(body)) ch.basic_ack(delivery_tag=method.delivery_tag)
def match(self, incoming: Order) -> MatchResult: """The specific logic is recorded in the module README. :param incoming: [description] :type incoming: Order :return: [description] :rtype: MatchResult """ mr = MatchResult() incoming.remaining_size = incoming.size candidates: ty.List[Order] = self.get_candidates(incoming) logger.debug(f"Found {len(candidates)} resting orders as candidates") for candidate in candidates: candidate.remaining_size = candidate.size if incoming.remaining_size <= 0: break else: # candidate's AON policy is respected within propose_trade; # if candidate is AON and incoming.remaining_size < candidate.size # then transaction is None transaction = self.propose_trade(incoming, candidate) if transaction is not None: logger.debug( f"{incoming} matches {candidate}: {transaction}") incoming.remaining_size -= transaction.size candidate.remaining_size -= transaction.size mr.transactions.append(transaction) mr.deactivated.append(candidate.order_id) # If the candidate is partially fulfilled, then create its # remains as a suborder if candidate.remaining_size > 0: mr.reactivated = candidate.create_suborder() # After all matchings are complete mr.incoming = incoming if incoming.remaining_size == incoming.size: logger.debug( f"No trade proposed; incoming order's remain is itself") mr.incoming_remain = mr.incoming mr.incoming_remain.active = mr.incoming.active = True elif incoming.remaining_size > 0: logger.debug( f"Incoming order partially fulfilled; creating suborder") mr.incoming_remain = incoming.create_suborder() mr.incoming_remain.active = True else: logger.debug(f"Incoming order completely fulfilled") mr.incoming_remain = None # Respect the AON and IOC policy if incoming.all_or_none and incoming.remaining_size > 0: logger.debug( f"Incoming order is all-or-none and less than completely fulfilled" ) mr.incoming_remain = mr.incoming mr.incoming_remain.active = True mr.transactions = [] # There is no need to refresh the candidate since each match() # reads a fresh record from the database, which means the mutations # in one match cycle does not persist to the next mr.deactivated = [] mr.reactivated = None if incoming.immediate_or_cancel and (mr.incoming_remain is not None): logger.debug( f"Incoming order is immediate-or-cancel and has non-trivial remains" ) mr.incoming_remain.cancelled_dttm = dt.datetime.utcnow() mr.incoming_remain.active = False return mr