def test_match_recommendations_fake_offer_bid(self, market, two_sided_market_matching): """Test the case when an offer or bid which don't belong to market is sent.""" bid = Bid("bid_id1", pendulum.now(), price=2, energy=1, buyer="B") offer = Offer("id", pendulum.now(), price=2, energy=1, seller="other") market.bids = {"bid_id1": bid} recommendations = [ BidOfferMatch(bids=[bid.serializable_dict()], offers=[offer.serializable_dict()], trade_rate=2, selected_energy=1, market_id=market.id).serializable_dict() ] # The sent offer isn't in market offers, should be skipped market.match_recommendations(recommendations) assert len(market.trades) == 0 assert not market.validate_bid_offer_match.called assert not market.accept_bid_offer_pair.called assert not market._replace_offers_bids_with_residual_in_recommendations_list.called market.offers = {offer.id: offer} market.bids = {} # The sent bid isn't in market offers, should be skipped market.match_recommendations(recommendations) assert len(market.trades) == 0 assert not market.validate_bid_offer_match.called assert not market.accept_bid_offer_pair.called assert not market._replace_offers_bids_with_residual_in_recommendations_list.called
def test_match_recommendations_one_offer_multiple_bids(self, market): """Test match_recommendations() method of TwoSidedMarket using 1 offer N bids.""" bid1 = Bid("bid_id1", pendulum.now(), price=1, energy=1, buyer="Buyer") bid2 = Bid("bid_id2", pendulum.now(), price=1, energy=1, buyer="Buyer") offer1 = Offer("offer_id1", pendulum.now(), price=2, energy=2, seller="Seller") market.bids = {"bid_id1": bid1, "bid_id2": bid2} market.offers = {"offer_id1": offer1} recommendations = [ BidOfferMatch( bids=[bid.serializable_dict() for bid in market.bids.values()], offers=[ offer.serializable_dict() for offer in market.offers.values() ], trade_rate=1, selected_energy=1, market_id=market.id).serializable_dict() ] market.match_recommendations(recommendations) assert len(market.trades) == 2
def test_matching_list_gets_updated_with_residual_offers(self): matches = [ BidOfferMatch(offers=[ Offer("offer_id", pendulum.now(), 1, 1, "S").serializable_dict() ], selected_energy=1, bids=[ Bid("bid_id", pendulum.now(), 1, 1, "B").serializable_dict() ], trade_rate=1, market_id="").serializable_dict(), BidOfferMatch(offers=[ Offer("offer_id2", pendulum.now(), 2, 2, "S").serializable_dict() ], selected_energy=2, bids=[ Bid("bid_id2", pendulum.now(), 2, 2, "B").serializable_dict() ], trade_rate=1, market_id="").serializable_dict() ] offer_trade = Trade("trade", 1, Offer("offer_id", pendulum.now(), 1, 1, "S"), "S", "B", residual=Offer("residual_offer", pendulum.now(), 0.5, 0.5, "S")) bid_trade = Trade("bid_trade", 1, Bid("bid_id2", pendulum.now(), 1, 1, "S"), "S", "B", residual=Bid("residual_bid_2", pendulum.now(), 1, 1, "S")) matches = TwoSidedMarket._replace_offers_bids_with_residual_in_recommendations_list( matches, offer_trade, bid_trade) assert len(matches) == 2 assert matches[0]["offers"][0]["id"] == "residual_offer" assert matches[1]["bids"][0]["id"] == "residual_bid_2"
def match_recommendations( self, recommendations: List[BidOfferMatch.serializable_dict]) -> None: """Match a list of bid/offer pairs, create trades and residual offers/bids.""" while recommendations: recommended_pair = recommendations.pop(0) recommended_pair = BidOfferMatch.from_dict(recommended_pair) selected_energy = recommended_pair.selected_energy clearing_rate = recommended_pair.trade_rate market_offers = [ self.offers.get(offer["id"]) for offer in recommended_pair.offers ] market_bids = [ self.bids.get(bid["id"]) for bid in recommended_pair.bids ] if not all(market_offers) and all(market_bids): # If not all offers bids exist in the market, skip the current recommendation continue self.validate_bid_offer_match(market_bids, market_offers, clearing_rate, selected_energy) market_offers = iter(market_offers) market_bids = iter(market_bids) market_offer = next(market_offers, None) market_bid = next(market_bids, None) while market_bid and market_offer: original_bid_rate = market_bid.original_bid_price / market_bid.energy trade_bid_info = TradeBidOfferInfo( original_bid_rate=original_bid_rate, propagated_bid_rate=market_bid.energy_rate, original_offer_rate=market_offer.original_offer_price / market_offer.energy, propagated_offer_rate=market_offer.energy_rate, trade_rate=original_bid_rate) bid_trade, offer_trade = self.accept_bid_offer_pair( market_bid, market_offer, clearing_rate, trade_bid_info, min(selected_energy, market_offer.energy, market_bid.energy)) if offer_trade.residual: market_offer = offer_trade.residual else: market_offer = next(market_offers, None) if bid_trade.residual: market_bid = bid_trade.residual else: market_bid = next(market_bids, None) recommendations = ( self. _replace_offers_bids_with_residual_in_recommendations_list( recommendations, offer_trade, bid_trade))
def _get_validated_recommendations( self, recommendations: List[Dict] ) -> List[BidOfferMatch.serializable_dict]: """Return a validated list of BidOfferMatch instances.""" validated_recommendations = [] for recommendation in recommendations: if not BidOfferMatch.is_valid_dict(recommendation): raise MycoValidationException("BidOfferMatch is not valid") market = self.markets_mapping.get(recommendation.get("market_id")) if market is None: # The market doesn't exist raise MycoValidationException( f"Market with id {recommendation.get('market_id')} doesn't exist." ) if market.readonly: # The market is already finished # TODO: we're clearing markets cache after each market cycle so is this check # TODO: .. really relevant? (All finished markets should be deleted already) raise MycoValidationException( "Cannot match trades in a finished market.") # Get the original bid and offer from the market market_bids = [ market.bids.get(bid.get("id")) for bid in recommendation.get("bids") ] market_offers = [ market.offers.get(offer.get("id")) for offer in recommendation.get("offers") ] if not (all(market_bids) and all(market_offers)): # Offers or Bids either don't belong to market or were already matched continue try: market.validate_bid_offer_match( market_bids, market_offers, recommendation.get("trade_rate"), recommendation.get("selected_energy")) except InvalidBidOfferPairException: continue recommendation["bids"] = [ bid.serializable_dict() for bid in market_bids ] recommendation["offers"] = [ offer.serializable_dict() for offer in market_offers ] validated_recommendations.append(recommendation) return validated_recommendations
def perform_pay_as_bid_match(market_offers_bids_list_mapping): """Performs pay as bid matching algorithm. There are 2 simplistic approaches to the problem 1. Match the cheapest offer with the most expensive bid. This will favor the sellers 2. Match the cheapest offer with the cheapest bid. This will favor the buyers, since the most affordable offers will be allocated for the most aggressive buyers. Args: market_offers_bids_list_mapping: dict {market_uuid: {"offers": [...], "bids": [...]}, } Returns: List[BidOfferMatch.serializable_dict()] """ bid_offer_pairs = [] for market_id, data in market_offers_bids_list_mapping.items(): bids = data.get("bids") offers = data.get("offers") # Sorted bids in descending order sorted_bids = sort_list_of_dicts_by_attribute(bids, "energy_rate", True) # Sorted offers in descending order sorted_offers = sort_list_of_dicts_by_attribute( offers, "energy_rate", True) already_selected_bids = set() for offer in sorted_offers: for bid in sorted_bids: if bid.get("id") in already_selected_bids or\ offer.get("seller") == bid.get("buyer"): continue if (offer.get("energy_rate") - bid.get("energy_rate")) <= FLOATING_POINT_TOLERANCE: already_selected_bids.add(bid.get("id")) selected_energy = min(bid.get("energy"), offer.get("energy")) bid_offer_pairs.append( BidOfferMatch(market_id=market_id, bid=bid, offer=offer, selected_energy=selected_energy, trade_rate=bid.get( "energy_rate")).serializable_dict()) break return bid_offer_pairs
def test_match_recommendations(self, market): """Test match_recommendations() method of TwoSidedMarket.""" bid = Bid("bid_id1", pendulum.now(), price=2, energy=1, buyer="Buyer") offer = Offer("offer_id1", pendulum.now(), price=2, energy=1, seller="Seller") market.bids = {"bid_id1": bid} market.offers = {"offer_id1": offer} recommendations = [ BidOfferMatch(bids=[bid.serializable_dict()], offers=[offer.serializable_dict()], trade_rate=2, selected_energy=1, market_id=market.id).serializable_dict() ] market.match_recommendations(recommendations) assert len(market.trades) == 1