def setUp(self): order_id = OrderId(TraderId(b'3' * 20), OrderNumber(1)) self.ask_order = Order( order_id, AssetPair(AssetAmount(5, 'BTC'), AssetAmount(6, 'EUR')), Timeout(3600), Timestamp.now(), True) self.bid_order = Order( order_id, AssetPair(AssetAmount(5, 'BTC'), AssetAmount(6, 'EUR')), Timeout(3600), Timestamp.now(), False) self.queue = MatchPriorityQueue(self.ask_order)
def __init__(self, community, order): super(MatchCache, self).__init__(community.request_cache, "match", int(order.order_id.order_number)) self.community = community self.order = order self.matches = {} self.schedule_propose = None self.schedule_task = None self.schedule_task_done = False self.outstanding_requests = [] self.received_responses_ids = set() self.queue = MatchPriorityQueue(self.order)
def test_priority(self): """ Test the priority mechanism of the queue """ order_id = OrderId(TraderId(b'1' * 20), OrderNumber(1)) self.queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(0, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(2, Price(1, 1, 'DUM1', 'DUM2'), order_id) item1 = self.queue.delete() item2 = self.queue.delete() item3 = self.queue.delete() self.assertEqual(item1[0], 0) self.assertEqual(item2[0], 1) self.assertEqual(item3[0], 2) # Same retries, different prices self.queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id) item1 = self.queue.delete() item2 = self.queue.delete() item3 = self.queue.delete() self.assertEqual(item1[1], Price(1, 1, 'DUM1', 'DUM2')) self.assertEqual(item2[1], Price(1, 2, 'DUM1', 'DUM2')) self.assertEqual(item3[1], Price(1, 3, 'DUM1', 'DUM2')) # Test with bid order self.queue = MatchPriorityQueue(self.bid_order) self.queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id) item1 = self.queue.delete() item2 = self.queue.delete() item3 = self.queue.delete() self.assertEqual(item1[1], Price(1, 3, 'DUM1', 'DUM2')) self.assertEqual(item2[1], Price(1, 2, 'DUM1', 'DUM2')) self.assertEqual(item3[1], Price(1, 1, 'DUM1', 'DUM2'))
def test_priority(bid_order, queue): """ Test the priority mechanism of the queue """ order_id = OrderId(TraderId(b'1' * 20), OrderNumber(1)) other_order_id = OrderId(TraderId(b'2' * 20), OrderNumber(1)) queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) queue.insert(0, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) queue.insert(2, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) item1 = queue.delete() item2 = queue.delete() item3 = queue.delete() assert item1[0] == 0 assert item2[0] == 1 assert item3[0] == 2 # Same retries, different prices queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id, other_order_id) queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id, other_order_id) item1 = queue.delete() item2 = queue.delete() item3 = queue.delete() assert item1[1] == Price(1, 1, 'DUM1', 'DUM2') assert item2[1] == Price(1, 2, 'DUM1', 'DUM2') assert item3[1] == Price(1, 3, 'DUM1', 'DUM2') # Test with bid order queue = MatchPriorityQueue(bid_order) queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id, other_order_id) queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id, other_order_id) item1 = queue.delete() item2 = queue.delete() item3 = queue.delete() assert item1[1] == Price(1, 3, 'DUM1', 'DUM2') assert item2[1] == Price(1, 2, 'DUM1', 'DUM2') assert item3[1] == Price(1, 1, 'DUM1', 'DUM2')
def queue(ask_order): return MatchPriorityQueue(ask_order)
class MatchCache(NumberCache): """ This cache keeps track of incoming match messages for a specific order. """ def __init__(self, community, order): super(MatchCache, self).__init__(community.request_cache, "match", int(order.order_id.order_number)) self.community = community self.order = order self.matches = {} self.schedule_propose = None self.schedule_task = None self.schedule_task_done = False self.outstanding_requests = [] self.received_responses_ids = set() self.queue = MatchPriorityQueue(self.order) @property def timeout_delay(self): return 7200.0 def on_timeout(self): pass def add_match(self, match_payload): """ Add a match to the queue. """ if self.order.status != "open": self._logger.info("Ignoring match payload, order %s not open anymore", self.order.order_id) return other_order_id = OrderId(match_payload.trader_id, match_payload.order_number) if other_order_id not in self.matches: self.matches[other_order_id] = [] # We do not want to add the match twice exists = False for match_payload in self.matches[other_order_id]: match_order_id = OrderId(match_payload.trader_id, match_payload.order_number) if match_order_id == other_order_id: exists = True break if not exists: self.matches[other_order_id].append(match_payload) if not self.queue.contains_order(other_order_id) and not self.has_outstanding_request_with_order_id( other_order_id): self._logger.debug("Adding match payload with own order id %s and other id %s to queue", self.order.order_id, other_order_id) self.queue.insert(0, match_payload.assets.price, other_order_id, match_payload.assets.first.amount) if not self.schedule_task: # Schedule a timer self._logger.info("Scheduling batch match of order %s" % str(self.order.order_id)) self.schedule_task = call_later(self.community.settings.match_window, self.start_process_matches, ignore_errors=True) elif self.schedule_task_done and not self.outstanding_requests: # If we are currently not processing anything and the schedule task is done, process the matches self.process_match() def start_process_matches(self): """ Start processing the batch of matches. """ self.schedule_task_done = True self._logger.info("Processing incoming matches for order %s", self.order.order_id) # It could be that the order has already been completed while waiting - we should let the matchmaker know if self.order.status != "open": self._logger.info("Order %s is already fulfilled - notifying matchmakers", self.order.order_id) for _, matches in self.matches.items(): for match_payload in matches: # Send a declined trade back other_order_id = OrderId(match_payload.trader_id, match_payload.order_number) self.community.send_decline_match_message(self.order, other_order_id, match_payload.matchmaker_trader_id, DeclineMatchReason.ORDER_COMPLETED) self.matches = {} return self.process_match() def process_match(self): """ Process the first eligible match. First, we sort the list based on price. """ items_processed = 0 while self.order.available_quantity > 0 and not self.queue.is_empty(): item = self.queue.delete() retries, price, other_order_id, other_quantity = item self.outstanding_requests.append(item) if retries == 0: propose_quantity = min(self.order.available_quantity, other_quantity) self.order.reserve_quantity_for_tick(other_order_id, propose_quantity) self.community.order_manager.order_repository.update(self.order) ensure_future(self.community.accept_match_and_propose(self.order, other_order_id, price, other_quantity, propose_quantity=propose_quantity, should_reserve=False)) else: task_id = "%s-%s" % (self.order.order_id, other_order_id) if not self.community.is_pending_task_active(task_id): delay = random.uniform(1, 2) self.community.register_task(task_id, self.community.accept_match_and_propose, self.order, other_order_id, price, other_quantity, delay=delay) items_processed += 1 if items_processed == self.community.settings.match_process_batch_size: # Limit the number of outgoing items when processing break self._logger.debug("Processed %d items in this batch", items_processed) def received_decline_trade(self, other_order_id, decline_reason): """ The counterparty refused to trade - update the cache accordingly. """ self.received_responses_ids.add(other_order_id) if decline_reason == DeclinedTradeReason.ORDER_COMPLETED and other_order_id in self.matches: # Let the matchmakers know that the order is complete for match_payload in self.matches[other_order_id]: self.community.send_decline_match_message(self.order, other_order_id, match_payload.matchmaker_trader_id, DeclineMatchReason.OTHER_ORDER_COMPLETED) elif decline_reason == DeclinedTradeReason.ORDER_CANCELLED and other_order_id in self.matches: # Let the matchmakers know that the order is cancelled for match_payload in self.matches[other_order_id]: self.community.send_decline_match_message(self.order, other_order_id, match_payload.matchmaker_trader_id, DeclineMatchReason.OTHER_ORDER_CANCELLED) elif decline_reason == DeclinedTradeReason.ADDRESS_LOOKUP_FAIL and other_order_id in self.matches: # Let the matchmakers know that the address resolution failed for match_payload in self.matches[other_order_id]: self.community.send_decline_match_message(self.order, other_order_id, match_payload.matchmaker_trader_id, DeclineMatchReason.OTHER) elif decline_reason in [DeclinedTradeReason.ORDER_RESERVED, DeclinedTradeReason.ALREADY_TRADING] and \ self.has_outstanding_request_with_order_id(other_order_id): # Add it to the queue again outstanding_request = self.get_outstanding_request_with_order_id(other_order_id) self._logger.debug("Adding entry (%d, %s, %s, %d) to matching queue again", *outstanding_request) self.queue.insert(outstanding_request[0] + 1, outstanding_request[1], outstanding_request[2], outstanding_request[3]) elif decline_reason == DeclinedTradeReason.NO_AVAILABLE_QUANTITY and \ self.has_outstanding_request_with_order_id(other_order_id): # Re-add the item to the queue, with the same priority outstanding_request = self.get_outstanding_request_with_order_id(other_order_id) self.queue.insert(outstanding_request[0], outstanding_request[1], outstanding_request[2], outstanding_request[3]) self.remove_outstanding_requests_with_order_id(other_order_id) if self.order.status == "open": self.process_match() def has_outstanding_request_with_order_id(self, order_id): for _, _, item_order_id, _ in self.outstanding_requests: if order_id == order_id: return True return False def get_outstanding_request_with_order_id(self, order_id): for item in self.outstanding_requests: _, _, item_order_id, _ = item if order_id == order_id: return item return None def remove_outstanding_requests_with_order_id(self, order_id): # Remove outstanding request entries with this order id to_remove = [] for item in self.outstanding_requests: _, _, item_order_id, _ = item if item_order_id == order_id: to_remove.append(item) for item in to_remove: self.outstanding_requests.remove(item) def remove_order(self, order_id): """ Remove all entries from the queue that match the passed order id. """ to_remove = [] for item in self.queue.queue: if item[2] == order_id: to_remove.append(item) for item in to_remove: self.queue.queue.remove(item) def did_trade(self, transaction, block): """ We just performed a trade with a counterparty. """ other_order_id = transaction.partner_order_id self.remove_outstanding_requests_with_order_id(other_order_id) if other_order_id not in self.matches: return self.received_responses_ids.add(other_order_id) for match_payload in self.matches[other_order_id]: self._logger.info("Sending transaction completed (order %s) to matchmaker %s", transaction.order_id, match_payload.matchmaker_trader_id.as_hex()) linked_block = self.community.trustchain.persistence.get_linked(block) or block global_time = self.community.claim_global_time() dist = GlobalTimeDistributionPayload(global_time) payload = HalfBlockPairPayload.from_half_blocks(block, linked_block) packet = self.community._ez_pack(self.community._prefix, MSG_MATCH_DONE, [dist, payload], False) self.community.endpoint.send(self.community.lookup_ip(match_payload.matchmaker_trader_id), packet) if self.order.status == "open": self.process_match()
class TestMatchQueue(unittest.TestCase): """ This class contains tests for the MatchingPriorityQueue object. """ def setUp(self): order_id = OrderId(TraderId(b'3' * 20), OrderNumber(1)) self.ask_order = Order( order_id, AssetPair(AssetAmount(5, 'BTC'), AssetAmount(6, 'EUR')), Timeout(3600), Timestamp.now(), True) self.bid_order = Order( order_id, AssetPair(AssetAmount(5, 'BTC'), AssetAmount(6, 'EUR')), Timeout(3600), Timestamp.now(), False) self.queue = MatchPriorityQueue(self.ask_order) def test_priority(self): """ Test the priority mechanism of the queue """ order_id = OrderId(TraderId(b'1' * 20), OrderNumber(1)) self.queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(0, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(2, Price(1, 1, 'DUM1', 'DUM2'), order_id) item1 = self.queue.delete() item2 = self.queue.delete() item3 = self.queue.delete() self.assertEqual(item1[0], 0) self.assertEqual(item2[0], 1) self.assertEqual(item3[0], 2) # Same retries, different prices self.queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id) item1 = self.queue.delete() item2 = self.queue.delete() item3 = self.queue.delete() self.assertEqual(item1[1], Price(1, 1, 'DUM1', 'DUM2')) self.assertEqual(item2[1], Price(1, 2, 'DUM1', 'DUM2')) self.assertEqual(item3[1], Price(1, 3, 'DUM1', 'DUM2')) # Test with bid order self.queue = MatchPriorityQueue(self.bid_order) self.queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id) self.queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id) item1 = self.queue.delete() item2 = self.queue.delete() item3 = self.queue.delete() self.assertEqual(item1[1], Price(1, 3, 'DUM1', 'DUM2')) self.assertEqual(item2[1], Price(1, 2, 'DUM1', 'DUM2')) self.assertEqual(item3[1], Price(1, 1, 'DUM1', 'DUM2'))