def test_iaa_event_bid_split_and_trade_correctly_populate_forwarded_bid_entries(iaa_bid, called, partial): iaa_bid.lower_market.delete_bid = called low_to_high_engine = iaa_bid.engines[0] iaa_bid._get_market_from_market_id = lambda x: low_to_high_engine.markets.target source_bid = list(low_to_high_engine.markets.source.bids.values())[0] target_bid = list(low_to_high_engine.markets.target.bids.values())[0] bidinfo = BidInfo(source_bid=source_bid, target_bid=target_bid) low_to_high_engine.forwarded_bids[source_bid.id] = bidinfo low_to_high_engine.forwarded_bids[target_bid.id] = bidinfo if partial: residual_energy = 0.2 residual_id = "resid" original_bid = Bid(*low_to_high_engine.markets.target._bids[0]) accepted_bid = original_bid._replace( price=(original_bid.energy-residual_energy) * (original_bid.price/original_bid.energy), energy=original_bid.energy-residual_energy) residual_bid = original_bid._replace( id=residual_id, price=residual_energy * (original_bid.price / original_bid.energy), energy=residual_energy) low_to_high_engine.event_bid_split(market_id=low_to_high_engine.markets.target, original_bid=original_bid, accepted_bid=accepted_bid, residual_bid=residual_bid) assert set(low_to_high_engine.forwarded_bids.keys()) == \ {original_bid.id, accepted_bid.id, residual_bid.id, "uuid", "id3", "id2"} else: original_bid = low_to_high_engine.markets.target._bids[0] accepted_bid = deepcopy(original_bid) residual_bid = None low_to_high_engine.event_bid_traded( bid_trade=Trade('trade_id', pendulum.now(tz=TIME_ZONE), accepted_bid, seller='someone_else', buyer='owner', residual=residual_bid)) if partial: # "id" gets traded in the target market, "id2" gets split in the source market, too assert set(low_to_high_engine.forwarded_bids.keys()) == {residual_bid.id, "uuid", "id3"} else: # "id" and "id2" get traded in both target and source, # left over is id3 and its forwarded instance uuid assert set(low_to_high_engine.forwarded_bids.keys()) == {"uuid", "id3"}
def test_iaa_event_trade_bid_updates_forwarded_bids_on_partial(iaa_bid, called, partial): iaa_bid.lower_market.delete_bid = called low_to_high_engine = iaa_bid.engines[0] iaa_bid._get_market_from_market_id = lambda x: low_to_high_engine.markets.target if partial: accepted_bid = Bid(*low_to_high_engine.markets.target._bids[0]) accepted_bid = \ accepted_bid._replace(price=(accepted_bid.energy-0.2) * (accepted_bid.price/accepted_bid.energy), energy=accepted_bid.energy-0.2) partial_bid = Bid('1234', 12, 0.2, 'owner', 'someone_else') low_to_high_engine.event_bid_changed(market_id=low_to_high_engine.markets.target, existing_bid=accepted_bid, new_bid=partial_bid) else: accepted_bid = low_to_high_engine.markets.target._bids[0] partial_bid = False source_bid = list(low_to_high_engine.markets.source.bids.values())[0] target_bid = list(low_to_high_engine.markets.target.bids.values())[0] bidinfo = BidInfo(source_bid=source_bid, target_bid=target_bid) low_to_high_engine.forwarded_bids[source_bid.id] = bidinfo low_to_high_engine.forwarded_bids[target_bid.id] = bidinfo low_to_high_engine.event_bid_traded( bid_trade=Trade('trade_id', pendulum.now(tz=TIME_ZONE), accepted_bid, seller='someone_else', buyer='owner', residual=partial_bid)) assert source_bid.id not in low_to_high_engine.forwarded_bids assert target_bid.id not in low_to_high_engine.forwarded_bids if partial: assert partial_bid.id in low_to_high_engine.forwarded_bids
def accept_bid(self, bid: Bid, energy: float = None, seller: str = None, buyer: str = None, already_tracked: bool = False, trade_rate: float = None, trade_offer_info=None, seller_origin=None): market_bid = self.bids.pop(bid.id, None) if market_bid is None: raise BidNotFound("During accept bid: " + str(bid)) seller = market_bid.seller if seller is None else seller buyer = market_bid.buyer if buyer is None else buyer energy = market_bid.energy if energy is None else energy orig_price = bid.original_bid_price if bid.original_bid_price is not None else bid.price residual_bid = None if energy <= 0: raise InvalidTrade("Energy cannot be negative or zero.") elif energy > market_bid.energy: raise InvalidTrade("Traded energy cannot be more than the bid energy.") elif energy < market_bid.energy: # partial bid trade accepted_bid, residual_bid = self.split_bid(market_bid, energy, orig_price) bid = accepted_bid # Delete the accepted bid from self.bids: try: self.bids.pop(accepted_bid.id) except KeyError: raise BidNotFound(f"Bid {accepted_bid.id} not found in self.bids ({self.name}).") else: # full bid trade, nothing further to do here pass fee_price, trade_price = self.determine_bid_price(trade_offer_info, energy) bid = bid._replace(price=trade_price) # Do not adapt grid fees when creating the bid_trade_info structure, to mimic # the behavior of the forwarded bids which use the source market fee. updated_bid_trade_info = self.fee_class.propagate_original_offer_info_on_bid_trade( trade_offer_info, ignore_fees=True ) trade = Trade(str(uuid.uuid4()), self.now, bid, seller, buyer, residual_bid, already_tracked=already_tracked, offer_bid_trade_info=updated_bid_trade_info, buyer_origin=bid.buyer_origin, seller_origin=seller_origin, fee_price=fee_price ) if already_tracked is False: self._update_stats_after_trade(trade, bid, bid.buyer, already_tracked) log.info(f"[TRADE][BID] [{self.name}] [{self.time_slot_str}] {trade}") self._notify_listeners(MarketEvent.BID_TRADED, bid_trade=trade) return trade
def accept_bid(self, bid: Bid, energy: float = None, seller: str = None, buyer: str = None, already_tracked: bool = False, trade_rate: float = None, trade_offer_info=None, seller_origin=None): market_bid = self.bids.pop(bid.id, None) if market_bid is None: raise BidNotFound("During accept bid: " + str(bid)) seller = market_bid.seller if seller is None else seller buyer = market_bid.buyer if buyer is None else buyer energy = market_bid.energy if energy is None else energy if trade_rate is None: trade_rate = market_bid.price / market_bid.energy assert trade_rate <= (market_bid.price / market_bid.energy) + FLOATING_POINT_TOLERANCE, \ f"trade rate: {trade_rate} market {market_bid.price / market_bid.energy}" orig_price = bid.original_bid_price if bid.original_bid_price is not None else bid.price residual = None if energy <= 0: raise InvalidTrade("Energy cannot be negative or zero.") elif energy > market_bid.energy: raise InvalidTrade("Traded energy cannot be more than the bid energy.") elif energy < market_bid.energy: # Partial bidding energy_portion = energy / market_bid.energy residual_price = (1 - energy_portion) * market_bid.price residual_energy = market_bid.energy - energy # Manually creating the bid in order to not double-charge the fee of the # residual bid changed_bid = Bid( str(uuid.uuid4()), residual_price, residual_energy, buyer, seller, original_bid_price=(1 - energy_portion) * orig_price, buyer_origin=market_bid.buyer_origin ) self.bids[changed_bid.id] = changed_bid self.bid_history.append(changed_bid) self._notify_listeners(MarketEvent.BID_CHANGED, existing_bid=bid, new_bid=changed_bid) residual = changed_bid revenue, fees, final_trade_rate = GridFees.calculate_trade_price_and_fees( trade_offer_info, self.transfer_fee_ratio ) self.market_fee += fees final_price = energy * final_trade_rate bid = Bid(bid.id, final_price, energy, buyer, seller, original_bid_price=energy_portion * orig_price, buyer_origin=bid.buyer_origin) else: revenue, fees, final_trade_rate = GridFees.calculate_trade_price_and_fees( trade_offer_info, self.transfer_fee_ratio ) self.market_fee += fees final_price = energy * final_trade_rate bid = bid._replace(price=final_price) trade = Trade(str(uuid.uuid4()), self._now, bid, seller, buyer, residual, already_tracked=already_tracked, offer_bid_trade_info=GridFees.propagate_original_offer_info_on_bid_trade( trade_offer_info, self.transfer_fee_ratio), buyer_origin=bid.buyer_origin, seller_origin=seller_origin ) if already_tracked is False: self._update_stats_after_trade(trade, bid, bid.buyer, already_tracked) log.info(f"[TRADE][BID] [{self.time_slot_str}] {trade}") final_bid = bid._replace(price=final_price) trade = trade._replace(offer=final_bid) self._notify_listeners(MarketEvent.BID_TRADED, bid_trade=trade) if not trade.residual: self._notify_listeners(MarketEvent.BID_DELETED, bid=market_bid) return trade