Exemple #1
0
 def __init__(self,
              time_slot=None,
              bc=None,
              notification_listener=None,
              readonly=False,
              transfer_fees: TransferFees = None,
              name=None):
     self.name = name
     self.bc = bc
     self.id = str(uuid.uuid4())
     self.time_slot = time_slot
     self.time_slot_str = time_slot.format(DATE_TIME_FORMAT) \
         if self.time_slot is not None \
         else None
     self.readonly = readonly
     # offer-id -> Offer
     self.offers = {}  # type: Dict[str, Offer]
     self.offer_history = []  # type: List[Offer]
     self.notification_listeners = []
     self.bids = {}  # type: Dict[str, Bid]
     self.bid_history = []  # type: List[Bid]
     self.trades = []  # type: List[Trade]
     self.transfer_fee_ratio = transfer_fees.grid_fee_percentage / 100 \
         if transfer_fees is not None else 0
     self.transfer_fee_const = transfer_fees.transfer_fee_const \
         if transfer_fees is not None else 0
     self.market_fee = 0
     # Store trades temporarily until bc event has fired
     self.traded_energy = {}
     self.accumulated_actual_energy_agg = {}
     self.min_trade_price = sys.maxsize
     self._avg_trade_price = None
     self.max_trade_price = 0
     self.min_offer_price = sys.maxsize
     self._avg_offer_price = None
     self.max_offer_price = 0
     self.accumulated_trade_price = 0
     self.accumulated_trade_energy = 0
     if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
         self.redis_publisher = MarketRedisEventPublisher(self.id)
     elif notification_listener:
         self.notification_listeners.append(notification_listener)
     self.current_tick = 0
     self.device_registry = DeviceRegistry.REGISTRY
     if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
         self.redis_api = MarketRedisEventSubscriber(self) \
             if ConstSettings.IAASettings.MARKET_TYPE == 1 \
             else TwoSidedMarketRedisEventSubscriber(self)
     setattr(self, RLOCK_MEMBER_NAME, RLock())
class TestMarketRedisEventPublisher(unittest.TestCase):
    def setUp(self):
        self.publisher = MarketRedisEventPublisher("test_id")

    def tearDown(self):
        pass

    def test_response_callback_stores_transaction_uuid(self):
        payload = {
            "data": json.dumps({
                "response": {},
                "transaction_uuid": "my_uuid"
            })
        }
        self.publisher.response_callback(payload)
        assert "my_uuid" in self.publisher.event_response_uuids

    def test_publish_event_subscribes_to_response_and_publishes(self):
        offer = Offer("1", now(), 2, 3, "A")
        trade = Trade("2", now(), Offer("accepted", now(), 7, 8, "Z"), "B",
                      "C")
        new_offer = Offer("3", now(), 4, 5, "D")
        existing_offer = Offer("4", now(), 5, 6, "E")
        kwargs = {
            "offer": offer,
            "trade": trade,
            "new_offer": new_offer,
            "existing_offer": existing_offer
        }

        self.publisher.publish_event(MarketEvent.OFFER, **kwargs)
        self.publisher.redis.sub_to_channel.assert_called_once_with(
            "market/test_id/notify_event/response",
            self.publisher.response_callback)

        expected_result = {k: v.to_JSON_string() for k, v in kwargs.items()}
        self.publisher.redis.publish.assert_called_once()
        assert self.publisher.redis.publish.call_args_list[0][0][0] == \
            "market/test_id/notify_event"
        publish_call_args = json.loads(
            self.publisher.redis.publish.call_args_list[0][0][1])
        assert publish_call_args["event_type"] == MarketEvent.OFFER.value
        assert len(DeepDiff(publish_call_args["kwargs"], expected_result)) == 0

    def test_wait_for_event_response_calls_poll_method(self):
        self.publisher.event_response_uuids = ["test_uuid"]
        self.publisher._wait_for_event_response(
            {"transaction_uuid": "test_uuid"})
        self.publisher.redis.poll_until_response_received.assert_called()
        assert "test_uuid" not in self.publisher.event_response_uuids
 def setUp(self):
     self.publisher = MarketRedisEventPublisher("test_id")
Exemple #4
0
class Market:

    def __init__(self, time_slot=None, bc=None, notification_listener=None, readonly=False,
                 transfer_fees: TransferFees = None, name=None):
        self.name = name
        self.bc = bc
        self.id = str(uuid.uuid4())
        self.time_slot = time_slot
        self.time_slot_str = time_slot.format(DATE_TIME_FORMAT) \
            if self.time_slot is not None \
            else None
        self.readonly = readonly
        # offer-id -> Offer
        self.offers = {}  # type: Dict[str, Offer]
        self.offer_history = []  # type: List[Offer]
        self.notification_listeners = []
        self.bids = {}  # type: Dict[str, Bid]
        self.bid_history = []  # type: List[Bid]
        self.trades = []  # type: List[Trade]
        self.transfer_fee_ratio = transfer_fees.transfer_fee_pct / 100 \
            if transfer_fees is not None else 0
        self.transfer_fee_const = transfer_fees.transfer_fee_const \
            if transfer_fees is not None else 0
        self.market_fee = 0
        # Store trades temporarily until bc event has fired
        self.traded_energy = {}
        self.accumulated_actual_energy_agg = {}
        self.min_trade_price = sys.maxsize
        self._avg_trade_price = None
        self.max_trade_price = 0
        self.min_offer_price = sys.maxsize
        self._avg_offer_price = None
        self.max_offer_price = 0
        self.accumulated_trade_price = 0
        self.accumulated_trade_energy = 0
        if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
            self.redis_publisher = MarketRedisEventPublisher(self.id)
        elif notification_listener:
            self.notification_listeners.append(notification_listener)
        self.current_tick = 0
        self.device_registry = DeviceRegistry.REGISTRY
        if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
            self.redis_api = MarketRedisEventSubscriber(self) \
                if ConstSettings.IAASettings.MARKET_TYPE == 1 \
                else TwoSidedMarketRedisEventSubscriber(self)
        setattr(self, RLOCK_MEMBER_NAME, RLock())

    def add_listener(self, listener):
        self.notification_listeners.append(listener)

    def _notify_listeners(self, event, **kwargs):
        if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
            self.redis_publisher.publish_event(event, **kwargs)
        else:
            # Deliver notifications in random order to ensure fairness
            for listener in sorted(self.notification_listeners, key=lambda l: random()):
                listener(event, market_id=self.id, **kwargs)

    def _update_stats_after_trade(self, trade, offer, buyer, already_tracked=False):
        # FIXME: The following updates need to be done in response to the BC event
        # TODO: For now event driven blockchain updates have been disabled in favor of a
        # sequential approach, but once event handling is enabled this needs to be handled
        if not already_tracked:
            self.trades.append(trade)
        self._update_accumulated_trade_price_energy(trade)
        self.traded_energy = add_or_create_key(self.traded_energy, offer.seller, offer.energy)
        self.traded_energy = subtract_or_create_key(self.traded_energy, buyer, offer.energy)
        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()

    def _update_accumulated_trade_price_energy(self, trade):
        self.accumulated_trade_price += trade.offer.price
        self.accumulated_trade_energy += trade.offer.energy

    def _update_min_max_avg_offer_prices(self):
        self._avg_offer_price = None
        offer_prices = [o.price / o.energy for o in self.offers.values()]
        if offer_prices:
            self.min_offer_price = round(min(offer_prices), 4)
            self.max_offer_price = round(max(offer_prices), 4)

    def _update_min_max_avg_trade_prices(self, price):
        self.max_trade_price = round(max(self.max_trade_price, price), 4)
        self.min_trade_price = round(min(self.min_trade_price, price), 4)
        self._avg_trade_price = None
        self._avg_offer_price = None

    def __repr__(self):  # pragma: no cover
        return "<Market{} offers: {} (E: {} kWh V: {}) trades: {} (E: {} kWh, V: {})>".format(
            " {}".format(self.time_slot_str),
            len(self.offers),
            sum(o.energy for o in self.offers.values()),
            sum(o.price for o in self.offers.values()),
            len(self.trades),
            self.accumulated_trade_energy,
            self.accumulated_trade_price
        )

    @staticmethod
    def sorting(obj, reverse_order=False):
        if reverse_order:
            # Sorted bids in descending order
            return list(reversed(sorted(
                obj.values(),
                key=lambda b: b.price / b.energy)))

        else:
            # Sorted bids in ascending order
            return list(sorted(
                obj.values(),
                key=lambda b: b.price / b.energy))

    @property
    def avg_offer_price(self):
        if self._avg_offer_price is None:
            price = sum(o.price for o in self.offers.values())
            energy = sum(o.energy for o in self.offers.values())
            self._avg_offer_price = round(price / energy, 4) if energy else 0
        return self._avg_offer_price

    @property
    def avg_trade_price(self):
        if self._avg_trade_price is None:
            price = self.accumulated_trade_price
            energy = self.accumulated_trade_energy
            self._avg_trade_price = round(price / energy, 4) if energy else 0
        return self._avg_trade_price

    @property
    def sorted_offers(self):
        return self.sorting(self.offers)

    @property
    def most_affordable_offers(self):
        cheapest_offer = self.sorted_offers[0]
        rate = cheapest_offer.price / cheapest_offer.energy
        return [o for o in self.sorted_offers if
                abs(o.price / o.energy - rate) < FLOATING_POINT_TOLERANCE]

    def update_clock(self, current_tick):
        self.current_tick = current_tick

    @property
    def now(self) -> DateTime:
        return GlobalConfig.start_date.add(
            seconds=GlobalConfig.tick_length.seconds * self.current_tick)

    def set_actual_energy(self, time, reporter, value):
        if reporter in self.accumulated_actual_energy_agg:
            self.accumulated_actual_energy_agg[reporter] += value
        else:
            self.accumulated_actual_energy_agg[reporter] = value

    @property
    def actual_energy_agg(self):
        return self.accumulated_actual_energy_agg

    def bought_energy(self, buyer):
        return sum(trade.offer.energy for trade in self.trades if trade.buyer == buyer)

    def sold_energy(self, seller):
        return sum(trade.offer.energy for trade in self.trades if trade.offer.seller == seller)

    def total_spent(self, buyer):
        return sum(trade.offer.price for trade in self.trades if trade.buyer == buyer)

    def total_earned(self, seller):
        return sum(trade.offer.price for trade in self.trades if trade.seller == seller)
Exemple #5
0
class Market:
    def __init__(self,
                 time_slot=None,
                 bc=None,
                 notification_listener=None,
                 readonly=False,
                 grid_fee_type=ConstSettings.IAASettings.GRID_FEE_TYPE,
                 grid_fees: GridFee = None,
                 name=None):
        self.name = name
        self.bc_interface = bc
        self.id = str(uuid.uuid4())
        self.time_slot = time_slot
        self.time_slot_str = time_slot.format(DATE_TIME_FORMAT) \
            if self.time_slot is not None \
            else None
        self.readonly = readonly
        # offer-id -> Offer
        self.offers = {}  # type: Dict[str, Offer]
        self.offer_history = []  # type: List[Offer]
        self.notification_listeners = []
        self.bids = {}  # type: Dict[str, Bid]
        self.bid_history = []  # type: List[Bid]
        self.trades = []  # type: List[Trade]
        self.const_fee_rate = None

        self._create_fee_handler(grid_fee_type, grid_fees)
        self.market_fee = 0
        # Store trades temporarily until bc event has fired
        self.traded_energy = {}
        self.min_trade_price = None
        self._avg_trade_price = None
        self.max_trade_price = None
        self.min_offer_price = None
        self._avg_offer_price = None
        self.max_offer_price = None
        self.accumulated_trade_price = 0
        self.accumulated_trade_energy = 0
        if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
            self.redis_publisher = MarketRedisEventPublisher(self.id)
        elif notification_listener:
            self.notification_listeners.append(notification_listener)
        self.current_tick_in_slot = 0
        self.device_registry = DeviceRegistry.REGISTRY
        if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
            self.redis_api = MarketRedisEventSubscriber(self) \
                if ConstSettings.IAASettings.MARKET_TYPE == 1 \
                else TwoSidedMarketRedisEventSubscriber(self)
        setattr(self, RLOCK_MEMBER_NAME, RLock())

    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)

    @property
    def _is_constant_fees(self):
        return isinstance(self.fee_class, ConstantGridFees)

    @property
    def open_bids_and_offers(self):
        return self.bids, self.offers

    def add_listener(self, listener):
        self.notification_listeners.append(listener)

    def _notify_listeners(self, event, **kwargs):
        if ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS:
            self.redis_publisher.publish_event(event, **kwargs)
        else:
            # Deliver notifications in random order to ensure fairness
            for listener in sorted(self.notification_listeners,
                                   key=lambda l: random()):
                listener(event, market_id=self.id, **kwargs)

    def _update_stats_after_trade(self,
                                  trade,
                                  offer_or_bid,
                                  already_tracked=False):
        # FIXME: The following updates need to be done in response to the BC event
        # TODO: For now event driven blockchain updates have been disabled in favor of a
        #  sequential approach, but once event handling is enabled this needs to be handled
        if not already_tracked:
            self.trades.append(trade)
            self.market_fee += trade.fee_price
        self._update_accumulated_trade_price_energy(trade)
        self.traded_energy = \
            add_or_create_key(self.traded_energy, trade.seller, offer_or_bid.energy)
        self.traded_energy = \
            subtract_or_create_key(self.traded_energy, trade.buyer, offer_or_bid.energy)
        self._update_min_max_avg_trade_prices(offer_or_bid.energy_rate)
        # Recalculate offer min/max price since offer was removed
        self._update_min_max_avg_offer_prices()

    def _update_accumulated_trade_price_energy(self, trade):
        self.accumulated_trade_price += trade.offer_bid.price
        self.accumulated_trade_energy += trade.offer_bid.energy

    def _update_min_max_avg_offer_prices(self):
        self._avg_offer_price = None
        offer_prices = [o.energy_rate for o in self.offers.values()]
        if offer_prices:
            self.min_offer_price = round(min(offer_prices), 4)
            self.max_offer_price = round(max(offer_prices), 4)

    def _update_min_max_avg_trade_prices(self, price):
        self.max_trade_price = round(max(self.max_trade_price, price), 4) if self.max_trade_price \
            else round(price, 4)
        self.min_trade_price = round(min(self.min_trade_price, price), 4) if self.min_trade_price \
            else round(price, 4)
        self._avg_trade_price = None
        self._avg_offer_price = None

    def __repr__(self):  # pragma: no cover
        return "<Market{} offers: {} (E: {} kWh V: {}) trades: {} (E: {} kWh, V: {})>".format(
            " {}".format(self.time_slot_str), len(self.offers),
            sum(o.energy for o in self.offers.values()),
            sum(o.price for o in self.offers.values()), len(self.trades),
            self.accumulated_trade_energy, self.accumulated_trade_price)

    @staticmethod
    def sorting(offers_bids: Dict,
                reverse_order=False) -> List[Union[Bid, Offer]]:
        """Sort a list of bids or offers by their energy_rate attribute."""
        if reverse_order:
            # Sorted bids in descending order
            return list(
                reversed(
                    sorted(offers_bids.values(),
                           key=lambda obj: obj.energy_rate)))
        else:

            return sorted(offers_bids.values(),
                          key=lambda obj: obj.energy_rate,
                          reverse=reverse_order)

    @property
    def avg_offer_price(self):
        if self._avg_offer_price is None:
            price = sum(o.price for o in self.offers.values())
            energy = sum(o.energy for o in self.offers.values())
            self._avg_offer_price = round(price / energy, 4) if energy else 0
        return self._avg_offer_price

    @property
    def avg_trade_price(self):
        if self._avg_trade_price is None:
            price = self.accumulated_trade_price
            energy = self.accumulated_trade_energy
            self._avg_trade_price = round(price / energy, 4) if energy else 0
        return self._avg_trade_price

    @property
    def sorted_offers(self):
        return self.sorting(self.offers)

    @property
    def most_affordable_offers(self):
        cheapest_offer = self.sorted_offers[0]
        rate = cheapest_offer.energy_rate
        return [
            o for o in self.sorted_offers
            if abs(o.energy_rate - rate) < FLOATING_POINT_TOLERANCE
        ]

    def update_clock(self, current_tick_in_slot):
        self.current_tick_in_slot = current_tick_in_slot

    @property
    def now(self) -> DateTime:
        return self.time_slot.add(seconds=GlobalConfig.tick_length.seconds *
                                  self.current_tick_in_slot)

    def bought_energy(self, buyer):
        return sum(trade.offer_bid.energy for trade in self.trades
                   if trade.buyer == buyer)

    def sold_energy(self, seller):
        return sum(trade.offer_bid.energy for trade in self.trades
                   if trade.seller == seller)

    def total_spent(self, buyer):
        return sum(trade.offer_bid.price for trade in self.trades
                   if trade.buyer == buyer)

    def total_earned(self, seller):
        return sum(trade.offer_bid.price for trade in self.trades
                   if trade.seller == seller)

    @property
    def info(self):
        return {
            "name": self.name,
            "id": self.id,
            "start_time": self.time_slot_str,
            "duration_min": GlobalConfig.slot_length.minutes
        }

    def get_bids_offers_trades(self):
        return {
            "bids": [b.serializable_dict() for b in self.bid_history],
            "offers": [o.serializable_dict() for o in self.offer_history],
            "trades": [t.serializable_dict() for t in self.trades]
        }