def accept_offer(self, offer_or_id: Union[str, BalancingOffer], buyer: str, *, energy: int = None, time: DateTime = None, already_tracked: bool = False, trade_rate: float = None, trade_bid_info: float = None, buyer_origin=None) -> BalancingTrade: if self.readonly: raise MarketReadOnlyException() if isinstance(offer_or_id, Offer): offer_or_id = offer_or_id.id offer = self.offers.pop(offer_or_id, None) if offer is None: raise OfferNotFoundException() if (offer.energy > 0 and energy < 0) or (offer.energy < 0 and energy > 0): raise InvalidBalancingTradeException("BalancingOffer and energy " "are not compatible") if energy is None: energy = offer.energy original_offer = offer residual_offer = None if trade_rate is None: trade_rate = offer.price / offer.energy orig_offer_price = self._calculate_original_prices(offer) try: if time is None: time = self.now if energy == 0: raise InvalidBalancingTradeException("Energy can not be zero.") elif abs(energy) < abs(offer.energy): # partial energy is requested assert trade_rate + FLOATING_POINT_TOLERANCE >= (offer.price / offer.energy) original_offer = offer accepted_offer, residual_offer = self.split_offer( offer, energy, orig_offer_price) fees, trade_price = self.determine_offer_price( energy / offer.energy, energy, already_tracked, trade_rate, trade_bid_info, orig_offer_price) offer = accepted_offer offer.price = trade_price elif abs(energy) > abs(offer.energy): raise InvalidBalancingTradeException( "Energy can't be greater than offered energy") else: # Requested energy is equal to offer's energy - just proceed normally fees, offer.price = self._update_offer_fee_and_calculate_final_price( energy, trade_rate, 1, orig_offer_price ) if already_tracked is False else energy * trade_rate except Exception: # Exception happened - restore offer self.offers[offer.id] = offer raise # Delete the accepted offer from self.offers: self.offers.pop(offer.id, None) trade_id, residual_offer = \ self.bc_interface.handle_blockchain_trade_event( offer, buyer, original_offer, residual_offer ) trade = BalancingTrade(trade_id, time, offer, offer.seller, buyer, residual_offer, seller_origin=offer.seller_origin, buyer_origin=buyer_origin, fee_price=fees) self.bc_interface.track_trade_event(trade) if already_tracked is False: self._update_stats_after_trade(trade, offer, buyer) log.info(f"[BALANCING_TRADE] [{self.time_slot_str}] {trade}") # TODO: Use non-blockchain non-event-driven version for now for both blockchain and # normal runs. self._notify_listeners(MarketEvent.BALANCING_TRADE, trade=trade) return trade
def event_trade(self, *, trade): offer_info = self.forwarded_offers.get(trade.offer.id) if not offer_info: # Trade doesn't concern us return if trade.offer.id == offer_info.target_offer.id: # Offer was accepted in target market - buy in source residual_info = None if trade.offer.energy < offer_info.source_offer.energy: try: residual_info = ResidualInfo( forwarded=self.trade_residual.pop(trade.offer.id), age=self.offer_age[offer_info.source_offer.id]) except KeyError: self.owner.log.error( "Not forwarding residual offer for " "{} (Forwarded offer not found)".format(trade.offer)) try: if trade.price_drop: # Use the rate of the trade offer for accepting the source offer too # Drop the rate of the trade offer according to IAA fee trade_offer_rate = trade.offer.price / trade.offer.energy offer_info.source_offer.price = \ trade_offer_rate * offer_info.source_offer.energy \ / (1 + (self.transfer_fee_pct / 100)) trade_source = self.owner.accept_offer( self.markets.source, offer_info.source_offer, energy=trade.offer.energy, buyer=self.owner.name) except OfferNotFoundException: raise OfferNotFoundException() self.owner.log.info( f"[{self.markets.source.time_slot_str}] Offer accepted {trade_source}" ) if residual_info is not None: # connect residual of the forwarded offer to that of the source offer if trade_source.residual is not None: if trade_source.residual.id not in self.forwarded_offers: res_offer_info = OfferInfo(trade_source.residual, residual_info.forwarded) self.forwarded_offers[ trade_source.residual.id] = res_offer_info self.forwarded_offers[ residual_info.forwarded.id] = res_offer_info self.offer_age[ trade_source.residual.id] = residual_info.age self.ignored_offers.add(trade_source.residual.id) else: self.owner.log.error( "Expected residual offer in source market trade {} - deleting " "corresponding offer in target market".format( trade_source)) self.markets.target.delete_offer(residual_info.forwarded) self._delete_forwarded_offer_entries(offer_info.source_offer) self.offer_age.pop(offer_info.source_offer.id, None) elif trade.offer.id == offer_info.source_offer.id: # Offer was bought in source market by another party try: self.markets.target.delete_offer(offer_info.target_offer) except OfferNotFoundException: pass except MarketException as ex: self.owner.log.error( "Error deleting InterAreaAgent offer: {}".format(ex)) self._delete_forwarded_offer_entries(offer_info.source_offer) self.offer_age.pop(offer_info.source_offer.id, None) else: raise RuntimeError("Unknown state. Can't happen") assert offer_info.source_offer.id not in self.forwarded_offers assert offer_info.target_offer.id not in self.forwarded_offers
def accept_offer(self, offer_or_id: Union[str, Offer], buyer: str, *, energy: int = None, time: DateTime = None, already_tracked: bool = False, trade_rate: float = None, trade_bid_info=None, buyer_origin=None, buyer_origin_id=None, buyer_id=None) -> Trade: if self.readonly: raise MarketReadOnlyException() if isinstance(offer_or_id, Offer): offer_or_id = offer_or_id.id offer = self.offers.pop(offer_or_id, None) if offer is None: raise OfferNotFoundException() if energy is None or isclose(energy, offer.energy, abs_tol=1e-8): energy = offer.energy original_offer = offer residual_offer = None if trade_rate is None: trade_rate = offer.energy_rate orig_offer_price = self._calculate_original_prices(offer) try: if time is None: time = self.now if energy == 0: raise InvalidTrade("Energy can not be zero.") elif energy < offer.energy: # partial energy is requested accepted_offer, residual_offer = self.split_offer( offer, energy, orig_offer_price) fee_price, trade_price = self.determine_offer_price( energy_portion=energy / accepted_offer.energy, energy=energy, trade_rate=trade_rate, trade_bid_info=trade_bid_info, orig_offer_price=orig_offer_price) offer = accepted_offer offer.update_price(trade_price) elif energy > offer.energy: raise InvalidTrade(f"Energy ({energy}) can't be greater than " f"offered energy ({offer.energy})") else: # Requested energy is equal to offer's energy - just proceed normally fee_price, trade_price = self.determine_offer_price( 1, energy, trade_rate, trade_bid_info, orig_offer_price) offer.update_price(trade_price) except Exception: # Exception happened - restore offer self.offers[offer.id] = offer raise trade_id, residual_offer = \ self.bc_interface.handle_blockchain_trade_event( offer, buyer, original_offer, residual_offer ) # Delete the accepted offer from self.offers: self.offers.pop(offer.id, None) offer_bid_trade_info = self.fee_class.propagate_original_bid_info_on_offer_trade( trade_original_info=trade_bid_info) trade = Trade(trade_id, time, offer, offer.seller, buyer, residual_offer, offer_bid_trade_info=offer_bid_trade_info, seller_origin=offer.seller_origin, buyer_origin=buyer_origin, fee_price=fee_price, buyer_origin_id=buyer_origin_id, seller_origin_id=offer.seller_origin_id, seller_id=offer.seller_id, buyer_id=buyer_id) self.bc_interface.track_trade_event(self.time_slot, trade) if already_tracked is False: self._update_stats_after_trade(trade, offer) log.info(f"[TRADE] [{self.name}] [{self.time_slot_str}] {trade}") # TODO: Use non-blockchain non-event-driven version for now for both blockchain and # normal runs. self._notify_listeners(MarketEvent.TRADE, trade=trade) return trade
def accept_offer(self, offer_or_id: Union[str, BalancingOffer], buyer: str, *, energy: int = None, time: DateTime = None, already_tracked: bool = False, trade_rate: float = None, trade_bid_info: float = None, buyer_origin=None) -> BalancingTrade: if self.readonly: raise MarketReadOnlyException() if isinstance(offer_or_id, Offer): offer_or_id = offer_or_id.id offer = self.offers.pop(offer_or_id, None) if offer is None: raise OfferNotFoundException() if (offer.energy > 0 and energy < 0) or (offer.energy < 0 and energy > 0): raise InvalidBalancingTradeException("BalancingOffer and energy " "are not compatible") if energy is None: energy = offer.energy original_offer = offer residual_offer = None if trade_rate is None: trade_rate = offer.price / offer.energy orig_offer_price = self._calculate_original_prices(offer) self._sorted_offers = sorted(self.offers.values(), key=lambda o: o.price / o.energy) try: if time is None: time = self._now energy_portion = energy / offer.energy if energy == 0: raise InvalidBalancingTradeException("Energy can not be zero.") # partial energy is requested elif abs(energy) < abs(offer.energy): original_offer = offer accepted_offer_id = offer.id \ if self.area is None or self.area.bc is None \ else offer.real_id assert trade_rate + FLOATING_POINT_TOLERANCE >= (offer.price / offer.energy) final_price = self._update_offer_fee_and_calculate_final_price( energy, trade_rate, energy_portion, orig_offer_price ) if already_tracked is False else energy * trade_rate accepted_offer = Offer(accepted_offer_id, abs(final_price), energy, offer.seller, seller_origin=offer.seller_origin) residual_price = (1 - energy_portion) * offer.price residual_energy = offer.energy - energy original_residual_price = \ ((offer.energy - energy) / offer.energy) * orig_offer_price residual_offer = Offer( str(uuid.uuid4()), abs(residual_price), residual_energy, offer.seller, original_offer_price=original_residual_price, seller_origin=offer.seller_origin) self.offers[residual_offer.id] = residual_offer log.debug(f"[BALANCING_OFFER][CHANGED][{self.time_slot_str}] " f"{original_offer} -> {residual_offer}") offer = accepted_offer self.bc_interface.change_offer(offer, original_offer, residual_offer) self._sorted_offers = sorted(self.offers.values(), key=lambda o: o.price / o.energy) self._notify_listeners(MarketEvent.BALANCING_OFFER_CHANGED, existing_offer=original_offer, new_offer=residual_offer) elif abs(energy) > abs(offer.energy): raise InvalidBalancingTradeException( "Energy can't be greater than offered energy") else: # Requested energy is equal to offer's energy - just proceed normally offer.price = self._update_offer_fee_and_calculate_final_price( energy, trade_rate, 1, orig_offer_price ) if already_tracked is False else energy * trade_rate except Exception: # Exception happened - restore offer self.offers[offer.id] = offer self._sorted_offers = sorted(self.offers.values(), key=lambda o: o.price / o.energy) raise trade_id, residual_offer = \ self.bc_interface.handle_blockchain_trade_event( offer, buyer, original_offer, residual_offer ) trade = BalancingTrade(trade_id, time, offer, offer.seller, buyer, residual_offer, seller_origin=offer.seller_origin, buyer_origin=buyer_origin) self.bc_interface.track_trade_event(trade) if already_tracked is False: self._update_stats_after_trade(trade, offer, buyer) log.info(f"[BALANCING_TRADE] [{self.time_slot_str}] {trade}") # TODO: Use non-blockchain non-event-driven version for now for both blockchain and # normal runs. self._notify_listeners(MarketEvent.BALANCING_TRADE, trade=trade) return trade
def event_trade(self, *, trade): offer_info = self.forwarded_offers.get(trade.offer.id) if not offer_info: # Trade doesn't concern us return if trade.offer.id == offer_info.target_offer.id: # Offer was accepted in target market - buy in source residual_info = None source_rate = offer_info.source_offer.price / offer_info.source_offer.energy target_rate = offer_info.target_offer.price / offer_info.target_offer.energy assert abs(source_rate) <= abs(target_rate) + FLOATING_POINT_TOLERANCE, \ f"offer: source_rate ({source_rate}) is not lower than target_rate ({target_rate})" if trade.offer.energy < offer_info.source_offer.energy: try: residual_info = ResidualInfo( forwarded=self.trade_residual.pop(trade.offer.id), age=self.offer_age[offer_info.source_offer.id]) except KeyError: self.owner.log.error( "Not forwarding residual offer for " "{} (Forwarded offer not found)".format(trade.offer)) try: trade_offer_rate = trade.offer.price / trade.offer.energy trade_source = self.owner.accept_offer( self.markets.source, offer_info.source_offer, energy=trade.offer.energy, buyer=self.owner.name, trade_rate=trade_offer_rate, trade_bid_info=GridFees. update_forwarded_offer_trade_original_info( trade.offer_bid_trade_info, offer_info.source_offer), buyer_origin=trade.buyer_origin) except OfferNotFoundException: raise OfferNotFoundException() self.owner.log.debug( f"[{self.markets.source.time_slot_str}] Offer accepted {trade_source}" ) if residual_info is not None: # connect residual of the forwarded offer to that of the source offer if trade_source.residual is not None: if trade_source.residual.id not in self.forwarded_offers: res_offer_info = OfferInfo(trade_source.residual, residual_info.forwarded) self.forwarded_offers[ trade_source.residual.id] = res_offer_info self.forwarded_offers[ residual_info.forwarded.id] = res_offer_info self.offer_age[ trade_source.residual.id] = residual_info.age self.ignored_offers.add(trade_source.residual.id) else: self.owner.log.warning( "Expected residual offer in source market trade {} - deleting " "corresponding offer in target market".format( trade_source)) self.owner.delete_offer(self.markets.target, residual_info.forwarded) self._delete_forwarded_offer_entries(offer_info.source_offer) self.offer_age.pop(offer_info.source_offer.id, None) elif trade.offer.id == offer_info.source_offer.id: # Offer was bought in source market by another party try: self.owner.delete_offer(self.markets.target, offer_info.target_offer) except OfferNotFoundException: pass except MarketException as ex: self.owner.log.error( "Error deleting InterAreaAgent offer: {}".format(ex)) self._delete_forwarded_offer_entries(offer_info.source_offer) self.offer_age.pop(offer_info.source_offer.id, None) else: raise RuntimeError("Unknown state. Can't happen") assert offer_info.source_offer.id not in self.forwarded_offers assert offer_info.target_offer.id not in self.forwarded_offers
def accept_offer(self, offer_or_id: Union[str, BalancingOffer], buyer: str, *, energy: int = None, time: DateTime = None, price_drop: bool = False) -> BalancingTrade: if self.readonly: raise MarketReadOnlyException() if isinstance(offer_or_id, Offer): offer_or_id = offer_or_id.id residual_offer = None offer = self.offers.pop(offer_or_id, None) self._sorted_offers = sorted(self.offers.values(), key=lambda o: o.price / o.energy) if offer is None: raise OfferNotFoundException() if (offer.energy > 0 and energy < 0) or (offer.energy < 0 and energy > 0): raise InvalidBalancingTradeException("BalancingOffer and energy " "are not compatible") try: if time is None: time = self._now if energy is not None: # Partial trade if energy == 0: raise InvalidBalancingTradeException( "Energy can not be zero.") elif abs(energy) < abs(offer.energy): original_offer = offer accepted_offer = Offer( offer.id, abs((offer.price / offer.energy) * energy), energy, offer.seller, offer.market) residual_energy = (offer.energy - energy) residual_offer = Offer( str(uuid.uuid4()), abs((offer.price / offer.energy) * residual_energy), residual_energy, offer.seller, offer.market) self.offers[residual_offer.id] = residual_offer log.info( f"[BALANCING_OFFER][CHANGED][{self.time_slot_str}] " f"{original_offer} -> {residual_offer}") offer = accepted_offer self._sorted_offers = sorted( self.offers.values(), key=lambda o: o.price / o.energy) self._notify_listeners(MarketEvent.BALANCING_OFFER_CHANGED, existing_offer=original_offer, new_offer=residual_offer) elif abs(energy) > abs(offer.energy): raise InvalidBalancingTradeException( f"Energy {energy} can't be greater than offered energy {offer}" ) else: # Requested partial is equal to offered energy - just proceed normally pass except Exception: # Exception happened - restore offer self.offers[offer.id] = offer self._sorted_offers = sorted(self.offers.values(), key=lambda o: o.price / o.energy) raise trade = BalancingTrade(id=str(uuid.uuid4()), time=time, offer=offer, seller=offer.seller, buyer=buyer, residual=residual_offer, price_drop=price_drop) self.trades.append(trade) self._update_accumulated_trade_price_energy(trade) log.warning(f"[BALANCING_TRADE][{self.time_slot_str}] {trade}") self.traded_energy[offer.seller] += offer.energy self.traded_energy[buyer] -= offer.energy self.ious[buyer][offer.seller] += offer.price self._update_min_max_avg_trade_prices(offer.price / offer.energy) # Recalculate offer min/max price since offer was removed self._update_min_max_avg_offer_prices() offer._traded(trade, self) self._notify_listeners(MarketEvent.BALANCING_TRADE, trade=trade) return trade
def accept_offer(self, offer_or_id: Union[str, Offer], buyer: str, *, energy: int = None, time: DateTime = None, already_tracked: bool = False, trade_rate: float = None, trade_bid_info=None, buyer_origin=None) -> Trade: if self.readonly: raise MarketReadOnlyException() if isinstance(offer_or_id, Offer): offer_or_id = offer_or_id.id offer = self.offers.pop(offer_or_id, None) if offer is None: raise OfferNotFoundException() if energy is None: energy = offer.energy original_offer = offer residual_offer = None if trade_rate is None: trade_rate = offer.price / offer.energy orig_offer_price = self._calculate_original_prices(offer) try: if time is None: time = self._now energy_portion = energy / offer.energy if energy == 0: raise InvalidTrade("Energy can not be zero.") # partial energy is requested elif energy < offer.energy: original_offer = offer accepted_offer_id = offer.id \ if self.area is None or self.area.bc is None \ else offer.real_id assert trade_rate + FLOATING_POINT_TOLERANCE >= (offer.price / offer.energy) if ConstSettings.IAASettings.MARKET_TYPE == 1: final_price = self._update_offer_fee_and_calculate_final_price( energy, trade_rate, energy_portion, orig_offer_price ) if already_tracked is False else energy * trade_rate else: revenue, fees, trade_rate_incl_fees = \ GridFees.calculate_trade_price_and_fees( trade_bid_info, self.transfer_fee_ratio ) self.market_fee += fees final_price = energy * trade_rate_incl_fees accepted_offer = Offer(accepted_offer_id, final_price, energy, offer.seller, seller_origin=offer.seller_origin) residual_price = (1 - energy_portion) * offer.price residual_energy = offer.energy - energy original_residual_price = \ ((offer.energy - energy) / offer.energy) * orig_offer_price residual_offer = Offer( str(uuid.uuid4()), residual_price, residual_energy, offer.seller, original_offer_price=original_residual_price, seller_origin=offer.seller_origin) self.offers[residual_offer.id] = residual_offer log.debug(f"[OFFER][CHANGED][{self.time_slot_str}] " f"{original_offer} -> {residual_offer}") offer = accepted_offer self.bc_interface.change_offer(offer, original_offer, residual_offer) self._notify_listeners(MarketEvent.OFFER_CHANGED, existing_offer=original_offer, new_offer=residual_offer) elif energy > offer.energy: raise InvalidTrade( "Energy can't be greater than offered energy") else: # Requested energy is equal to offer's energy - just proceed normally if ConstSettings.IAASettings.MARKET_TYPE == 1: offer.price = self._update_offer_fee_and_calculate_final_price( energy, trade_rate, 1, orig_offer_price ) if already_tracked is False else energy * trade_rate else: revenue, fees, trade_price = GridFees.calculate_trade_price_and_fees( trade_bid_info, self.transfer_fee_ratio) self.market_fee += fees offer.price = energy * trade_price except Exception: # Exception happened - restore offer self.offers[offer.id] = offer raise trade_id, residual_offer = \ self.bc_interface.handle_blockchain_trade_event( offer, buyer, original_offer, residual_offer ) trade = Trade(trade_id, time, offer, offer.seller, buyer, residual_offer, offer_bid_trade_info=GridFees. propagate_original_bid_info_on_offer_trade( trade_bid_info, self.transfer_fee_ratio), seller_origin=offer.seller_origin, buyer_origin=buyer_origin) self.bc_interface.track_trade_event(trade) if already_tracked is False: self._update_stats_after_trade(trade, offer, buyer) log.info(f"[TRADE] [{self.time_slot_str}] {trade}") # TODO: Use non-blockchain non-event-driven version for now for both blockchain and # normal runs. self._notify_listeners(MarketEvent.TRADE, trade=trade) return trade