def _create_fee_handler(self, grid_fee_type, grid_fees): if not grid_fees: grid_fees = GridFee(grid_fee_percentage=0.0, grid_fee_const=0.0) if grid_fee_type == 1: if grid_fees.grid_fee_const is None or \ grid_fees.grid_fee_const <= 0.0: self.fee_class = ConstantGridFees(0.0) else: self.fee_class = ConstantGridFees(grid_fees.grid_fee_const) self.const_fee_rate = self.fee_class.grid_fee_rate else: if grid_fees.grid_fee_percentage is None or \ grid_fees.grid_fee_percentage <= 0.0: self.fee_class = GridFees(0.0) else: self.fee_class = GridFees(grid_fees.grid_fee_percentage / 100)
def event_bid_traded(self, *, bid_trade): bid_info = self.forwarded_bids.get(bid_trade.offer.id) if not bid_info: return # Bid was traded in target market, buy in source if bid_trade.offer.id == bid_info.target_bid.id: market_bid = self.markets.source.bids[bid_info.source_bid.id] assert bid_trade.offer.energy <= market_bid.energy, \ f"Traded bid on target market has more energy than the market bid." source_rate = bid_info.source_bid.price / bid_info.source_bid.energy target_rate = bid_info.target_bid.price / bid_info.target_bid.energy assert source_rate >= target_rate, \ f"bid: source_rate ({source_rate}) is not lower than target_rate ({target_rate})" trade_rate = (bid_trade.offer.price/bid_trade.offer.energy) if bid_trade.offer_bid_trade_info is not None: # Adapt trade_offer_info received by the trade to include source market grid fees, # which was skipped when accepting the bid during the trade operation. updated_trade_offer_info = GridFees.propagate_original_offer_info_on_bid_trade( [None, None, *bid_trade.offer_bid_trade_info], self.markets.source.transfer_fee_ratio ) else: updated_trade_offer_info = bid_trade.offer_bid_trade_info source_trade = self.markets.source.accept_bid( market_bid, energy=bid_trade.offer.energy, seller=self.owner.name, already_tracked=False, trade_rate=trade_rate, trade_offer_info=GridFees.update_forwarded_bid_trade_original_info( updated_trade_offer_info, market_bid ), seller_origin=bid_trade.seller_origin ) self.after_successful_trade_event(source_trade, bid_info) # Bid was traded in the source market by someone else elif bid_trade.offer.id == bid_info.source_bid.id: self.delete_forwarded_bids(bid_info) else: raise Exception(f"Invalid bid state for IAA {self.owner.name}: " f"traded bid {bid_trade} was not in offered bids tuple {bid_info}")
def _create_fee_handler(self, grid_fee_type, transfer_fees): if not transfer_fees: transfer_fees = TransferFees(grid_fee_percentage=0.0, transfer_fee_const=0.0) if grid_fee_type == 1: if transfer_fees.transfer_fee_const is None or \ transfer_fees.transfer_fee_const <= 0.0: self.fee_class = ConstantGridFees(0.0) else: self.fee_class = ConstantGridFees( transfer_fees.transfer_fee_const) else: if transfer_fees.grid_fee_percentage is None or \ transfer_fees.grid_fee_percentage <= 0.0: self.fee_class = GridFees(0.0) else: self.fee_class = GridFees(transfer_fees.grid_fee_percentage / 100)
def __init__(self, offers, bids=[], m_id=123, transfer_fees=transfer_fees, name=None): self.name = name self.id = m_id self.offers = {o.id: o for o in offers} self._bids = bids self.bids = {bid.id: bid for bid in self._bids} self.offer_call_count = 0 self.bid_call_count = 0 self.forwarded_offer_id = 'fwd' self.forwarded_bid_id = 'fwd_bid_id' self.calls_energy = [] self.calls_energy_bids = [] self.calls_offers = [] self.calls_bids = [] self.calls_bids_price = [] self.time_slot = pendulum.now(tz=TIME_ZONE) self.time_slot_str = self.time_slot.format(TIME_FORMAT) self.state = MarketClearingState() self.fee_class = GridFees(transfer_fees.grid_fee_percentage)
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 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})" try: trade_offer_rate = trade.offer.price / trade.offer.energy updated_trade_bid_info = GridFees.update_forwarded_offer_trade_original_info( trade.offer_bid_trade_info, offer_info.source_offer) trade_source = self.owner.accept_offer( market_or_id=self.markets.source, offer=offer_info.source_offer, energy=trade.offer.energy, buyer=self.owner.name, trade_rate=trade_offer_rate, trade_bid_info=updated_trade_bid_info, buyer_origin=trade.buyer_origin) except OfferNotFoundException: raise OfferNotFoundException() self.owner.log.debug( f"[{self.markets.source.time_slot_str}] Offer accepted {trade_source}" ) 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 _update_new_offer_price_with_fee(self, offer_price, original_offer_price, energy): """ Override one sided market private method to abstract away the grid fee calculation when placing an offer to a market. :param offer_price: Price of the offer coming from the source market, in cents :param original_offer_price: Price of the original offer from the device :param energy: Not required here, added to comply with the one-sided market implementation :return: Updated price for the forwarded offer on this market """ return GridFees.update_incoming_offer_with_fee( offer_price, original_offer_price, self.transfer_fee_ratio )
def determine_offer_price(self, energy_portion, energy, trade_rate, trade_bid_info, orig_offer_price, original_offer, offer): if ConstSettings.IAASettings.MARKET_TYPE == 1: return self._update_offer_fee_and_calculate_final_price( energy, trade_rate, energy_portion, orig_offer_price) else: revenue, grid_fee_rate, trade_rate_incl_fees = \ GridFees.calculate_trade_price_and_fees( trade_bid_info, self.transfer_fee_ratio ) grid_fee_price = grid_fee_rate * energy return grid_fee_price, energy * (trade_rate_incl_fees - grid_fee_rate) \ if original_offer.seller_origin == offer.seller else \ energy * trade_rate_incl_fees
def match_offers_bids(self): if not (self.current_tick + 1) % int(self.mcp_update_point) == 0: return clearing = self._perform_pay_as_clear_matching() if clearing is None: return clearing_rate, clearing_energy = clearing if clearing_energy > 0: log.info(f"Market Clearing Rate: {clearing_rate} " f"||| Clearing Energy: {clearing_energy} " f"||| Clearing Market {self.name}") self.state.clearing[self.now] = (clearing_rate, clearing_energy) matchings = self._create_bid_offer_matchings(clearing_energy, self.sorted_offers, self.sorted_bids) for index, match in enumerate(matchings): offer = match.offer bid = match.bid assert math.isclose(match.offer_energy, match.bid_energy) selected_energy = match.offer_energy original_bid_rate = bid.original_bid_price / bid.energy propagated_bid_rate = bid.price / bid.energy offer_original_rate = offer.original_offer_price / offer.energy offer_propagated_rate = offer.price / offer.energy trade_rate_original = GridFees.calculate_original_trade_rate_from_clearing_rate( original_bid_rate, propagated_bid_rate, clearing_rate) trade_bid_info = TradeBidInfo( original_bid_rate=original_bid_rate, propagated_bid_rate=propagated_bid_rate, original_offer_rate=offer_original_rate, propagated_offer_rate=offer_propagated_rate, trade_rate=trade_rate_original) bid_trade, trade = self.accept_bid_offer_pair( bid, offer, clearing_rate, trade_bid_info, selected_energy) if trade.residual is not None or bid_trade.residual is not None: matchings = self._replace_offers_bids_with_residual_in_matching_list( matchings, index + 1, trade, bid_trade)
def _forward_bid(self, bid): if bid.buyer == self.markets.target.name and \ bid.seller == self.markets.source.name: return if self.owner.name == self.markets.target.name: return forwarded_bid = self.markets.target.bid( price=GridFees.update_forwarded_bid_with_fee( bid.price, bid.original_bid_price, self.markets.source.transfer_fee_ratio), energy=bid.energy, buyer=self.owner.name, seller=self.markets.target.name, original_bid_price=bid.original_bid_price, buyer_origin=bid.buyer_origin) self._add_to_forward_bids(bid, forwarded_bid) self.owner.log.trace(f"Forwarding bid {bid} to {forwarded_bid}") return forwarded_bid
def _forward_bid(self, bid): if bid.buyer == self.markets.target.name and \ bid.seller == self.markets.source.name: return if self.owner.name == self.markets.target.name: return forwarded_bid = self.markets.target.bid( GridFees.update_forwarded_bid_with_fee( bid.price, bid.original_bid_price, self.markets.source.transfer_fee_ratio), bid.energy, self.owner.name, self.markets.target.name, original_bid_price=bid.original_bid_price, buyer_origin=bid.buyer_origin ) bid_coupling = BidInfo(bid, forwarded_bid) self.forwarded_bids[forwarded_bid.id] = bid_coupling self.forwarded_bids[bid.id] = bid_coupling self.owner.log.trace(f"Forwarding bid {bid} to {forwarded_bid}") return forwarded_bid
def _forward_offer(self, offer, offer_id): if offer.price == 0: self.owner.log.debug("Offer is not forwarded because price=0") return forwarded_offer = self.markets.target.offer( GridFees.update_forwarded_offer_with_fee( offer.price, offer.original_offer_price, self.markets.target.transfer_fee_ratio), offer.energy, self.owner.name, offer.original_offer_price, dispatch_event=False, seller_origin=offer.seller_origin) offer_info = OfferInfo(deepcopy(offer), deepcopy(forwarded_offer)) self.forwarded_offers[forwarded_offer.id] = offer_info self.forwarded_offers[offer_id] = offer_info self.owner.log.trace(f"Forwarding offer {offer} to {forwarded_offer}") # TODO: Ugly solution, required in order to decouple offer placement from # new offer event triggering self.markets.target.dispatch_market_offer_event(offer) return forwarded_offer
def _offer_in_market(self, offer): kwargs = { "price": GridFees.update_forwarded_offer_with_fee( offer.price, offer.original_offer_price, self.markets.target.transfer_fee_ratio), "energy": offer.energy, "seller": self.owner.name, "original_offer_price": offer.original_offer_price, "dispatch_event": False, "seller_origin": offer.seller_origin } if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS: return self.owner.offer(market_id=self.markets.target, offer_args=kwargs) else: return self.markets.target.offer(**kwargs)
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
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
def _update_new_bid_price_with_fee(self, bid_price, original_bid_price): return GridFees.update_incoming_bid_with_fee(bid_price, original_bid_price, self.transfer_fee_ratio)
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_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 = GridFees.propagate_original_offer_info_on_bid_trade( trade_offer_info, 0.0) 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.time_slot_str}] {trade}") self._notify_listeners(MarketEvent.BID_TRADED, bid_trade=trade) return trade
def determine_bid_price(self, trade_offer_info, energy): revenue, grid_fee_rate, final_trade_rate = GridFees.calculate_trade_price_and_fees( trade_offer_info, self.transfer_fee_ratio) return grid_fee_rate * energy, energy * final_trade_rate
class FakeMarket: def __init__(self, offers, bids=[], m_id=123, transfer_fees=transfer_fees, name=None): self.name = name self.id = m_id self.offers = {o.id: o for o in offers} self._bids = bids self.bids = {bid.id: bid for bid in self._bids} self.offer_call_count = 0 self.bid_call_count = 0 self.forwarded_offer_id = 'fwd' self.forwarded_bid_id = 'fwd_bid_id' self.calls_energy = [] self.calls_energy_bids = [] self.calls_offers = [] self.calls_bids = [] self.calls_bids_price = [] self.time_slot = pendulum.now(tz=TIME_ZONE) self.time_slot_str = self.time_slot.format(TIME_FORMAT) self.state = MarketClearingState() self.fee_class = GridFees(transfer_fees.grid_fee_percentage) @property def sorted_offers(self): return list(sorted(self.offers.values(), key=lambda b: b.price / b.energy)) def get_bids(self): return self.bids def set_time_slot(self, timeslot): self.time_slot = timeslot def accept_offer(self, offer_or_id, buyer, *, energy=None, time=None, already_tracked=False, trade_rate: float = None, trade_bid_info=None, buyer_origin=None): offer = offer_or_id self.calls_energy.append(energy) self.calls_offers.append(offer) if energy < offer.energy: residual_energy = offer.energy - energy residual = Offer('res', offer.time, offer.price, residual_energy, offer.seller, seller_origin='res') traded = Offer(offer.id, offer.time, offer.price, energy, offer.seller, seller_origin='res') return Trade('trade_id', time, traded, traded.seller, buyer, residual, seller_origin=offer.seller_origin, buyer_origin=buyer_origin) else: return Trade('trade_id', time, offer, offer.seller, buyer, seller_origin=offer.seller_origin, buyer_origin=buyer_origin) def accept_bid(self, bid, energy, seller, buyer=None, *, time=None, trade_rate: float = None, trade_offer_info=None, already_tracked=False, seller_origin=None): self.calls_energy_bids.append(energy) self.calls_bids.append(bid) self.calls_bids_price.append(bid.price) if trade_rate is None: trade_rate = bid.price / bid.energy else: assert trade_rate <= (bid.price / bid.energy) market_bid = [b for b in self._bids if b.id == bid.id][0] if energy < market_bid.energy: residual_energy = bid.energy - energy residual = Bid('res', bid.time, bid.price, residual_energy, bid.buyer, seller, buyer_origin='res') traded = Bid(bid.id, bid.time, (trade_rate * energy), energy, bid.buyer, seller, buyer_origin='res') return Trade('trade_id', time, traded, traded.seller, bid.buyer, residual, buyer_origin=bid.buyer_origin, seller_origin=seller_origin) else: traded = Bid(bid.id, bid.time, (trade_rate * energy), energy, bid.buyer, seller, buyer_origin=bid.id) return Trade('trade_id', time, traded, traded.seller, bid.buyer, buyer_origin=bid.buyer_origin, seller_origin=seller_origin) def delete_offer(self, *args): pass def delete_bid(self, *args): pass def _update_new_offer_price_with_fee(self, offer_price, original_offer_price, energy): return offer_price + self.fee_class.grid_fee_rate * original_offer_price def _update_new_bid_price_with_fee(self, bid_price, original_bid_price): return self.fee_class.update_incoming_bid_with_fee( bid_price, original_bid_price) def offer(self, price: float, energy: float, seller: str, offer_id=None, original_offer_price=None, dispatch_event=True, seller_origin=None, adapt_price_with_fees=True) -> Offer: self.offer_call_count += 1 if original_offer_price is None: original_offer_price = price if offer_id is None: offer_id = "uuid" if adapt_price_with_fees: price = self._update_new_offer_price_with_fee(price, original_offer_price, energy) offer = Offer(offer_id, pendulum.now(), price, energy, seller, original_offer_price, seller_origin=seller_origin) self.offers[offer.id] = deepcopy(offer) self.forwarded_offer = deepcopy(offer) return offer def dispatch_market_offer_event(self, offer): pass def bid(self, price: float, energy: float, buyer: str, seller: str, bid_id: str = None, original_bid_price=None, buyer_origin=None, adapt_price_with_fees=True): self.bid_call_count += 1 if original_bid_price is None: original_bid_price = price if bid_id is None: bid_id = "uuid" if adapt_price_with_fees: price = self._update_new_bid_price_with_fee(price, original_bid_price) bid = Bid(bid_id, pendulum.now(), price, energy, buyer, seller, original_bid_price=original_bid_price, buyer_origin=buyer_origin) self._bids.append(bid) self.forwarded_bid = bid return bid def split_offer(self, original_offer, energy, orig_offer_price): self.offers.pop(original_offer.id, None) # same offer id is used for the new accepted_offer accepted_offer = self.offer(offer_id=original_offer.id, price=original_offer.price * (energy / original_offer.energy), energy=energy, seller=original_offer.seller, dispatch_event=False, seller_origin=original_offer.seller_origin) residual_price = (1 - energy / original_offer.energy) * original_offer.price residual_energy = original_offer.energy - energy original_residual_price = \ ((original_offer.energy - energy) / original_offer.energy) * orig_offer_price residual_offer = self.offer(price=residual_price, energy=residual_energy, seller=original_offer.seller, original_offer_price=original_residual_price, dispatch_event=False, seller_origin=original_offer.seller_origin, adapt_price_with_fees=False) return accepted_offer, residual_offer def split_bid(self, original_bid, energy, orig_bid_price): self.offers.pop(original_bid.id, None) # same offer id is used for the new accepted_offer accepted_bid = self.bid(bid_id=original_bid.id, buyer=original_bid.buyer, price=original_bid.price * (energy / original_bid.energy), energy=energy, seller=original_bid.seller, buyer_origin=original_bid.buyer_origin) residual_price = (1 - energy / original_bid.energy) * original_bid.price residual_energy = original_bid.energy - energy original_residual_price = \ ((original_bid.energy - energy) / original_bid.energy) * orig_bid_price residual_bid = self.bid(price=residual_price, buyer=original_bid.buyer, energy=residual_energy, seller=original_bid.seller, original_bid_price=original_residual_price, buyer_origin=original_bid.buyer_origin, adapt_price_with_fees=False) return accepted_bid, residual_bid
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 if energy == 0: raise InvalidTrade("Energy can not be zero.") elif energy < offer.energy: # partial energy is requested assert trade_rate + FLOATING_POINT_TOLERANCE >= (offer.price / offer.energy) 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, original_offer=original_offer, offer=accepted_offer) offer = accepted_offer offer.price = trade_price 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 fee_price, offer.price = self.determine_offer_price( 1, energy, trade_rate, trade_bid_info, orig_offer_price, original_offer, offer) 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 = GridFees.propagate_original_bid_info_on_offer_trade( trade_original_info=trade_bid_info, tax_ratio=self.transfer_fee_ratio) 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) 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