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
Beispiel #2
0
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
Beispiel #6
0
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