Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
 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
Ejemplo n.º 7
0
    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