Ejemplo n.º 1
0
    def __init__(self, market_id, **kwargs):
        self.server_online = False
        self.run = True
        # Initialize timing intervals and definitions
        self.__status = {
            'active_participants': 0,
            'round_metered': 0,
            'round_matched': False,
            'round_settled': [],
            'round_settle_delivered': []
        }
        self.__timing = {
            'mode': 'sim',
            'timezone': kwargs['timezone'],
            'current_round': (0, 60),
            'duration': 60,
            'last_round': (0, 0),
            'close_steps':
            kwargs['close_steps'] if 'close_steps' in kwargs else 2
            # close steps = 2 for 1 step-ahead market agent debugging

            # Ideally close_steps should be 16 for a 15-step ahead market.
            # bids and asks are settled 15 steps ahead of delivery time
            # settle takes 1 step after bid/ask submision
        }

        self.__db = {}
        self.save_transactions = True
        self.market_id = market_id
        self.__client = kwargs['sio_client']
        self.__server_ts = 0

        self.__clients = {}
        self.__participants = {}

        self.__grid = Grid(**kwargs['grid_params'])

        self.__open = {'non_dispatch': {}, 'dispatch': {}}
        self.__settled = {}
        self.__transactions = []
        self.__transaction_last_record_time = 0
        self.transactions_count = 0
Ejemplo n.º 2
0
class Market:
    """MicroTE is a futures trading based market design for transactive energy as part of TREX

    The market mechanism here works more like standard futures contracts,
    where delivery time interval is submitted along with the bid or ask.

    bids and asks are organized by source type
    the addition of delivery time requires that the bids and asks to be further organized by time slot

    Bids/asks can be are accepted for any time slot starting from one step into the future to infinity
    The minimum close slot is determined by 'close_steps', where a close_steps of 2 is 1 step into the future
    The the minimum close time slot is the last delivery slot that will accept bids/asks

    """
    def __init__(self, market_id, **kwargs):
        self.server_online = False
        self.run = True
        # Initialize timing intervals and definitions
        self.__status = {
            'active_participants': 0,
            'round_metered': 0,
            'round_matched': False,
            'round_settled': [],
            'round_settle_delivered': []
        }
        self.__timing = {
            'mode': 'sim',
            'timezone': kwargs['timezone'],
            'current_round': (0, 60),
            'duration': 60,
            'last_round': (0, 0),
            'close_steps':
            kwargs['close_steps'] if 'close_steps' in kwargs else 2
            # close steps = 2 for 1 step-ahead market agent debugging

            # Ideally close_steps should be 16 for a 15-step ahead market.
            # bids and asks are settled 15 steps ahead of delivery time
            # settle takes 1 step after bid/ask submision
        }

        self.__db = {}
        self.save_transactions = True
        self.market_id = market_id
        self.__client = kwargs['sio_client']
        self.__server_ts = 0

        self.__clients = {}
        self.__participants = {}

        self.__grid = Grid(**kwargs['grid_params'])

        self.__open = {'non_dispatch': {}, 'dispatch': {}}
        self.__settled = {}
        self.__transactions = []
        self.__transaction_last_record_time = 0
        self.transactions_count = 0

    def __time(self):
        """Return time based on time convention

        Market timing operates in two modes: real-time, and simulation.
        In real-time mode, the market has control of timing, and
        in simulation mode, the simulation controller has control
        Because of this, the way time propagates through the system is slightly different between modes

        In real-time mode, master time is acquired from the system clock of the market
        In simulation mode, master time is the last time tuple that was received from the simulation controller
        """
        if self.__timing['mode'] == 'rt':
            return calendar.timegm(time.gmtime())
        if self.__timing['mode'] == 'sim':
            return self.__server_ts

    def mode_switch(self, mode):
        """Switch timing modes between real-time mode and simulation mode

        """
        self.__timing['mode'] = mode

    async def open_db(self, db_string, table_name):
        if not self.save_transactions:
            return

        self.__db['path'] = db_string
        self.__db['table_name'] = table_name

        if 'table' not in self.__db or self.__db['table'] is None:
            table_name = self.__db.pop('table_name') + '_market'
            await db_utils.create_table(db_string=db_string,
                                        table_type='market',
                                        table_name=table_name)
            self.__db['table'] = db_utils.get_table(db_string, table_name)

    async def register(self):
        """Function that attempts to register Market client with socket.io server in the market namespace

        """
        async def register_cb(success):
            if success:
                self.server_online = True

        client_data = {'type': ('market', 'MicroTE'), 'id': self.market_id}
        await self.__client.emit('register',
                                 client_data,
                                 namespace='/market',
                                 callback=register_cb)

    async def participant_connected(self, client_data):
        if client_data['id'] not in self.__participants:
            self.__participants[client_data['id']] = {
                'sid': client_data['sid'],
                'online': True,
                'meter': {}
            }
        else:
            # if previously registered participant returned, update with new session ID and toggle online status
            self.__participants[client_data['id']].update({
                'sid':
                client_data['sid'],
                'online':
                True
            })
        self.__clients[client_data['sid']] = client_data['id']
        self.__status['active_participants'] = min(
            self.__status['active_participants'] + 1, len(self.__participants))
        return self.market_id, client_data['sid']

    async def participant_disconnected(self, participant_id):
        # if a registered participant disconnects for any reason, switch online status to off
        self.__participants[participant_id].update({'online': False})
        self.__clients.pop(self.__participants[participant_id]['sid'], None)
        self.__status['active_participants'] -= 1

    async def __classify_source(self, source):
        return await source_classifier.classify(source)

    # Initialize variables for new time step
    def __reset_status(self):
        self.__status['round_metered'] = 0
        self.__status['round_matched'] = False
        self.__status['round_settled'].clear()
        self.__status['round_settle_delivered'].clear()

    async def __start_round(self, duration):
        """
        Message all participants the start of the current round, as well as the duration

        Because having somewhat synchronized timing is key to proper market operation, the start round message mostly
        contains useful time intervals. Additional info that are deemed useful can be included,
        such as grid prices that change with time.
        As always, it is advised to keep the message length minimal to maximize performance and to conserve bandwidth.

        Participants can take the times in this message and determine clock differences and communication delays.
        Will be necessary in real-time to ensure actions are received by the market before the start of the next round,
        as the market does not wait in real-time mode
        """
        start_time = self.__time()
        self.__reset_status()
        market_info = {
            str(self.__timing['current_round']): {
                'grid': {
                    'buy_price': self.__grid.buy_price(),
                    'sell_price': self.__grid.sell_price()
                }
            },
            str(self.__timing['next_settle']): {
                'grid': {
                    'buy_price': self.__grid.buy_price(),
                    'sell_price': self.__grid.sell_price()
                }
            },
        }

        start_msg = {
            'time': start_time,
            'duration': duration,
            'timezone': self.__timing['timezone'],
            'last_round': self.__timing['last_round'],
            'current_round': self.__timing['current_round'],
            'last_settle': self.__timing['last_settle'],
            'next_settle': self.__timing['next_settle'],
            'market_info': market_info,
        }
        await self.__client.emit('start_round', start_msg, namespace='/market')

    async def submit_entry(self, message: dict, entry_type: str):
        """Processes bids/asks sent from the participants

        If action from participants are valid, then an entry will be made on the market for matching.
        In all cases, a confirmation message will be sent back to the sender indicating success or failure.
        The handling of the confirmation message is up to the participant.

        If the message and entry_type are valid, an open record will be made in the time delivery slot for source type.
        the record is a dictionary containing the following:

        - 'uuid'
        - 'participant_id'
        - 'session_id'
        - 'source'
        - 'price'
        - 'time_submission'
        - 'quantity'
        - 'lock'

        Note: as of April 1, 2020, 'lock' is not being used in simulation mode.
        Deprecation in general is under consideration

        Parameters
        ----------
        message : dict
            Message should be a dictionary containing the following:

            - 'participant_id'
            - 'quantity' (quantity in Wh)
            - 'price' (price in $/kWh)
            - 'source' (such as 'solar', 'bess', 'wind', etc. Must be classifiable)
            - 'time_delivery'

        entry_type : str
            Must be either 'bid' or 'ask'

        Returns
        -------
        confirmation
            returns the participant session id and confirmation message for SIO server callback

            - For all invalid entries, confirmation message is a dictionary with 'uuid' as the key and None as the value
            - For all valid entries, confirmation message be a dictionary containing the following:

                - 'uuid'
                - 'time_submission'
                - 'source'
                - 'price'
                - 'quantity'
                - 'time_delivery'

        """

        if entry_type not in {'bid', 'ask'}:
            # raise Exception('invalid action')
            return message['session_id'], {'uuid': None}

        # entry validity check step 1: quantity must be positive
        if message['quantity'] <= 0:
            # raise Exception('quantity must be a positive integer')
            return message['session_id'], {'uuid': None}

        # entry validity check step 2: source must be classifiable
        source_type = await self.__classify_source(message['source'])
        if not source_type:
            # raise Exception('quantity must be a positive integer')
            return message['session_id'], {'uuid': None}

        # if entry is valid, then update entry with market specific info
        # convert kwh price to token price

        entry = {
            'uuid': cuid(),
            'participant_id': message['participant_id'],
            'session_id': message['session_id'],
            'source': message['source'],
            'price': message['price'],
            'time_submission': self.__time(),
            'quantity': message['quantity'],
            'lock': False
        }

        # create a new time slot container if the time slot doesn't exist
        time_delivery = tuple(message['time_delivery'])
        if time_delivery not in self.__open[source_type]:
            self.__open[source_type][time_delivery] = {entry_type: []}

        # if the time slot exists but no entry exist, create the entry container
        if entry_type not in self.__open[source_type][time_delivery]:
            self.__open[source_type][time_delivery][entry_type] = []

        # add open entry
        self.__open[source_type][time_delivery][entry_type].append(entry)

        reply = {
            'uuid': entry['uuid'],
            'time_submission': entry['time_submission'],
            'source': entry['source'],
            'price': entry['price'],
            'quantity': entry['quantity'],
            'time_delivery': time_delivery
        }

        return message['session_id'], reply

    async def __match(self, source_type, time_delivery):
        """Matches bids with asks for a single source type in a time slot

        THe matching and settlement process closely resemble double auctions.
        For all bids/asks for a source in the delivery time slots, highest bids are matched with lowest asks
        and settled pairwise. Quantities can be partially settled. Unsettled quantities are discarded. Participants are only obligated to buy/sell quantities settled for the delivery period.

        Parameters
        ----------
        source_type: str
            Indicates the energy type pool to perform matching. Can be either 'dispatch' or 'non_dispatch'

            Depending on specific market implementations, the sequence of matching matters. As the current iteration of the market allows dispatchable sources to compensate for inaccurate non-dispatch generation, dispatch must be matched before non-dispatch.

        time_delivery : tuple
            Tuple containing the start and end timestamps in UNIX timestamp format indicating the interval for energy to be delivered. 

        Notes
        -----
        Presently, the settlement price is hard-coded as the average price of the bid/ask pair. In the near future, dedicated, more sophisticated functions for determining settlement price will be implemented

        """

        if time_delivery not in self.__open[source_type]:
            return

        if 'ask' not in self.__open[source_type][time_delivery]:
            return

        if 'bid' not in self.__open[source_type][time_delivery]:
            return

        # remove zero-quantity bid and ask entries
        # sort bids by decreasing price and asks by increasing price
        self.__open[source_type][time_delivery]['ask'][:] = \
            sorted([ask for ask in self.__open[source_type][time_delivery]['ask'] if ask['quantity'] > 0],
                   key=itemgetter('price'), reverse=False)
        self.__open[source_type][time_delivery]['bid'][:] = \
            sorted([bid for bid in self.__open[source_type][time_delivery]['bid'] if bid['quantity'] > 0],
                   key=itemgetter('price'), reverse=True)

        bids = self.__open[source_type][time_delivery]['bid']
        asks = self.__open[source_type][time_delivery]['ask']

        for bid, ask, in itertools.product(bids, asks):
            if ask['price'] != bid['price']:
                continue

            if bid['participant_id'] == ask['participant_id']:
                continue

            if bid['source'] != ask['source']:
                continue

            if bid['lock'] or ask['lock']:
                continue

            if bid['quantity'] <= 0 or ask['quantity'] <= 0:
                continue

            if bid['participant_id'] not in self.__participants:
                bid['lock'] = True
                continue

            if ask['participant_id'] not in self.__participants:
                ask['lock'] = True
                continue

            # Settle highest price bids with lowest price asks
            await self.__settle(bid, ask, time_delivery)

    async def __settle(self,
                       bid: dict,
                       ask: dict,
                       time_delivery: tuple,
                       settlement_price=None,
                       locking=False):
        """Performs settlement for bid/ask pairs found during the matching process.

        If bid/ask are valid, the bid/ask quantities are adjusted, a commitment record is created, and a settlement confirmation is sent to both participants. 

        Parameters
        ----------
        bid: dict
            bid entry to be settled. Should be a reference to the open bid

        ask: dict
            bid entry to be settled. Should be a reference to the open ask

        time_delivery : tuple
            Tuple containing the start and end timestamps in UNIX timestamp format.

        locking: bool
        Optinal locking mode, which locks the bid and ask until a callback is received after settlement confirmation is sent. The default value is False.

        Currently, locking should be disabled in simulation mode, as waiting for callback causes some settlements to be incomplete, likely due a flaw in the implementation or a poor understanding of how callbacks affect the sequence of events to be executed in async mode.

        Notes
        -----
        It is possible to settle directly with the grid, although this feature is currently not used by the agents and is under consideration to be deprecated.


        """

        # if bid is 'grid', this means settling ask with grid (selling to grid)
        # if ask is 'grid', this means setting bid with grid (buying from grid)
        # bid and ask cannot be 'grid' at the same time
        if bid['source'] == 'grid' and ask['source'] == 'grid':
            return

        # only proceed to settle if settlement quantity is positive
        quantity = min(bid['quantity'], ask['quantity'])
        if quantity <= 0:
            return

        if locking:
            # lock the bid and ask until confirmations are received
            ask['lock'] = True
            bid['lock'] = True

        commit_id = cuid()
        settlement_time = self.__timing['current_round'][1]
        if not settlement_price:
            settlement_price = (ask['price'] + bid['price']) / 2
        record = {
            'quantity': quantity,
            'seller_id': ask['participant_id'],
            'buyer_id': bid['participant_id'],
            'energy_source': ask['source'],
            'settlement_price': settlement_price,
            'time_purchase': settlement_time
        }

        # Record successful settlements
        if time_delivery not in self.__settled:
            self.__settled[time_delivery] = {}

        self.__settled[time_delivery][commit_id] = {
            'time_settlement': settlement_time,
            'source': ask['source'],
            'record': record,
            'ask': ask,
            'seller_id': ask['participant_id'],
            'bid': bid,
            'buyer_id': bid['participant_id'],
            'lock': locking
        }

        message = {
            'commit_id': commit_id,
            'bid_id': bid['uuid'],
            'ask_id': ask['uuid'],
            'source': ask['source'],
            'quantity': quantity,
            'price': settlement_price,
            'buyer_id': bid['participant_id'],
            'seller_id': ask['participant_id'],
            'time_delivery': time_delivery
        }

        if locking:
            await self.__client.emit('send_settlement',
                                     message,
                                     namespace='/market',
                                     callback=self.__settle_confirm_lock)
        else:
            await self.__client.emit('send_settlement',
                                     message,
                                     namespace='/market')
            bid['quantity'] = max(
                0, bid['quantity'] -
                self.__settled[time_delivery][commit_id]['record']['quantity'])
            ask['quantity'] = max(
                0, ask['quantity'] -
                self.__settled[time_delivery][commit_id]['record']['quantity'])
        self.__status['round_settled'].append(commit_id)

    # after settlement confirmation, update bid and ask quantities
    async def settlement_delivered(self, commit_id):
        self.__status['round_settle_delivered'].append(commit_id)

    async def __settle_confirm_lock(self, message):
        """Callback for settle in locking mode

        """

        time_delivery = tuple(message['time_delivery'])
        if time_delivery not in self.__settled:
            return

        commit_id = message['commit_id']
        ask = self.__settled[time_delivery][commit_id]['ask']
        bid = self.__settled[time_delivery][commit_id]['bid']

        ask['lock'] = not message['seller']
        bid['lock'] = not message['buyer']

        if not ask['lock'] and not bid['lock']:
            self.__settled[time_delivery][commit_id]['lock'] = False
            bid['quantity'] = max(
                0, bid['quantity'] -
                self.__settled[time_delivery][commit_id]['record']['quantity'])
            ask['quantity'] = max(
                0, ask['quantity'] -
                self.__settled[time_delivery][commit_id]['record']['quantity'])

    async def meter_data(self, message):
        """Update meter data from participant

        Meter data should be received from participants at the end of the each round for delivery.
        """

        # meter = {
        #     'time_interval': (),
        #     'generation': {
        #         'solar': 0,
        #         'bess': 0
        #     },
        #     'consumption': {
        #         'bess': {
        #             'solar': 0,
        #         },
        #         'other': {
        #             'solar': 0,
        #             'bess': 0,
        #             'other': 0
        #         }
        #     }
        # }

        # TODO: add data validation later
        participant_id = message['participant_id']
        time_delivery = tuple(message['meter']['time_interval'])
        self.__participants[participant_id]['meter'][time_delivery] = message[
            'meter']
        self.__status['round_metered'] += 1

    async def __process_settlements(self, time_delivery, source_type):
        physical_tranactions = []
        financial_transactions = []
        settlements = self.__settled[time_delivery]
        for buyer in self.__participants:
            for seller in self.__participants:
                if buyer == seller:
                    continue
                # make sure the buyer and seller are online
                if not self.__participants[buyer]['online']:
                    continue
                if not self.__participants[seller]['online']:
                    continue

                # Extract settlements involving buyer and seller (that are not locked)
                relevant_settlements = {
                    k: v
                    for (k, v) in settlements.items()
                    if settlements[k]['lock'] is False
                    and settlements[k]['buyer_id'] == buyer
                    and settlements[k]['seller_id'] == seller
                }

                if relevant_settlements:
                    for commit_id in relevant_settlements.keys():
                        energy_source = self.__settled[time_delivery][
                            commit_id]['source']
                        energy_type = await self.__classify_source(
                            energy_source)
                        if energy_type != source_type:
                            continue

                        settled_quantity = self.__settled[time_delivery][
                            commit_id]['record']['quantity']
                        if not settled_quantity:
                            continue
                        residual_generation = self.__participants[seller][
                            'meter'][time_delivery]['generation'][
                                energy_source]
                        residual_consumption = self.__participants[buyer][
                            'meter'][time_delivery]['consumption']['other'][
                                'external']

                        # check to see if physical generation is less than settled quantity
                        # extra_purchase = 0
                        deficit_generation = max(
                            0, settled_quantity - residual_generation)
                        # Add on the amount that needed to be bought from the grid?
                        # self.__participants[buyer]['meter']['consumption']['other']['external'] += deficit_generation
                        # if not deficit_generation:
                        # check if settled quantity is greater than residual consumption
                        # if settled amount is greater than residual generation, then figure out
                        # the financial compensation.
                        extra_purchase = max(
                            0, settled_quantity - residual_consumption)
                        # print(settled_quantity, energy_source, residual_generation, residual_consumption, extra_purchase, deficit_generation)
                        pt, ft = await self.__transfer_energy(
                            time_delivery, commit_id, extra_purchase,
                            deficit_generation)
                        physical_tranactions.extend(pt)
                        financial_transactions.extend(ft)
        return physical_tranactions, financial_transactions

    # async def __process_self_consumption(self, participant_id):

    async def __scrub_financial_transaction(self, transactions):
        scrubbed_transactions = {}
        for transaction in transactions:
            if transaction['seller_id'] not in scrubbed_transactions:
                scrubbed_transactions[transaction['seller_id']] = {
                    'buy': [],
                    'sell': []
                }
            if transaction['buyer_id'] not in scrubbed_transactions:
                scrubbed_transactions[transaction['buyer_id']] = {
                    'buy': [],
                    'sell': []
                }
            scrubbed_transaction = {
                'quantity': transaction['quantity'],
                'energy_source': transaction['energy_source'],
                'settlement_price': transaction['settlement_price'],
                'time_creation': transaction['time_creation'],
                'time_purchase': transaction['time_purchase']
            }
            scrubbed_transactions[transaction['buyer_id']]['buy'].append(
                scrubbed_transaction)
            scrubbed_transactions[transaction['seller_id']]['sell'].append(
                scrubbed_transaction)
        return scrubbed_transactions

    async def __process_energy_exchange(self, time_delivery):
        """The main function for finalizing energy exchange using settlements and meter data.

        Energy exchange takes the following steps in order of priority:

        1. Exchange settled energy
        2. Process self-consumption
        3. Process residual energy

        Aside from perfect settlements (i.e, ), which are not expected in realistic scen
        
        There are five possible scenarios for each settlement:

        1. Seller's residual generation is the exact amount as settled
        2. Seller's residual generation is less than settled
        3. Seller's residual generation is more than settled
        4. Buyer's residual consumption is the exact amount as settled
        5. Buyer's residual consumption is less than settled
        6. Buyer's residual consumption is more than settled

        Aside from 1 and 4, all other scenarios require additional handling.
        
        - Scenario 2: The seller must either pay for the shortage from the grid, or compensate by injecting the shortage from their BESS. BESS compensation must be done prior to sending meter data.
        - Scenario 3: The residual are sold to the grid at grid prices
        - Scenario 5: The buyer must pay the seller the full amount of the settlement. The residual generation cannot be sold to the grid again, as that would be double compensation.
        - Scenario 6: The buyer must buy the residual consumption from the grid at grid prices.

        On top of properly balancing the market, these schemes should also provide sufficient punishment that drive the agents to make more optimal decisions.

        """
        # print('-----')
        # STEP 1
        # process auction deliveries
        transactions = []
        financial_transactions = []
        # Step 1: exchange settled
        if time_delivery in self.__settled:
            for source_type in {'dispatch', 'non_dispatch'}:
                # important: dispatch must be first!!!
                pt, ft = await self.__process_settlements(
                    time_delivery, source_type)
                transactions.extend(pt + ft)
                financial_transactions.extend(ft)

        scrubbed_financial_transactions = await self.__scrub_financial_transaction(
            financial_transactions)

        # Steps 2 & 3
        # process self-consumption
        # process residual energy
        for participant_id in self.__participants:
            if not self.__participants[participant_id]['meter']:
                continue

            if time_delivery not in self.__participants[participant_id][
                    'meter']:
                print(participant_id, 'not metered')
                continue

            # self consumption
            for load in self.__participants[participant_id]['meter'][
                    time_delivery]['consumption']:
                for source in self.__participants[participant_id]['meter'][
                        time_delivery]['consumption'][load]:
                    if source in self.__participants[participant_id]['meter'][
                            time_delivery]['generation']:
                        # assuming everything is perfectly sub metered
                        quantity = self.__participants[participant_id][
                            'meter'][time_delivery]['consumption'][load][
                                source]

                        if quantity > 0:
                            transaction_record = {
                                'quantity': quantity,
                                'seller_id': participant_id,
                                'buyer_id': participant_id,
                                'energy_source': source,
                                'settlement_price': 0,
                                'time_creation': time_delivery[0],
                                'time_purchase': time_delivery[1],
                                'time_consumption': time_delivery[1]
                            }
                            transactions.append(transaction_record.copy())
                            self.__participants[participant_id]['meter'][
                                time_delivery]['consumption'][load][
                                    source] -= quantity

            extra_transactions = {
                'participant': participant_id,
                'time_delivery': time_delivery,
                'grid': {
                    'buy': [],
                    'sell': []
                }
            }
            # sell residual generation(s) to the grid
            for source in self.__participants[participant_id]['meter'][
                    time_delivery]['generation']:
                residual_generation = self.__participants[participant_id][
                    'meter'][time_delivery]['generation'][source]
                if residual_generation > 0:
                    transaction_record = {
                        'quantity': residual_generation,
                        'seller_id': participant_id,
                        'buyer_id': self.__grid.id,
                        'energy_source': source,
                        'settlement_price': self.__grid.sell_price(),
                        'time_creation': time_delivery[0],
                        'time_purchase': time_delivery[1],
                        'time_consumption': time_delivery[1]
                    }
                    transactions.append(transaction_record.copy())
                    self.__participants[participant_id]['meter'][
                        time_delivery]['generation'][
                            source] -= residual_generation
                    extra_transactions['grid']['sell'].append(
                        transaction_record.copy())
            # buy residual consumption (other) from grid
            residual_consumption = self.__participants[participant_id][
                'meter'][time_delivery]['consumption']['other']['external']
            if residual_consumption > 0:
                transaction_record = {
                    'quantity': residual_consumption,
                    'seller_id': self.__grid.id,
                    'buyer_id': participant_id,
                    'energy_source': 'grid',
                    'settlement_price': self.__grid.buy_price(),
                    'time_creation': time_delivery[0],
                    'time_purchase': time_delivery[1],
                    'time_consumption': time_delivery[1]
                }
                transactions.append(transaction_record.copy())
                self.__participants[participant_id]['meter'][time_delivery][
                    'consumption']['other']['external'] -= residual_consumption
                extra_transactions['grid']['buy'].append(
                    transaction_record.copy())

            if participant_id in scrubbed_financial_transactions:
                extra_transactions[
                    'financial'] = scrubbed_financial_transactions[
                        participant_id]

            await self.__client.emit(event='return_extra_transactions',
                                     data=extra_transactions,
                                     namespace='/market')
        if self.save_transactions:
            self.__transactions.extend(transactions)
            await self.record_transactions(10000)

    async def __transfer_energy(self,
                                time_delivery,
                                commit_id,
                                extra_purchase=0,
                                deficit_generation=0):
        # pt, ft = await self.__transfer_energy(time_delivery, commit_id, extra_purchase, deficit_generation)
        """This function makes the energy transaction records for each settlement

        """

        physical_transactions = []
        financial_transactions = []
        seller_id = self.__settled[time_delivery][commit_id]['seller_id']
        buyer_id = self.__settled[time_delivery][commit_id]['buyer_id']
        energy_source = self.__settled[time_delivery][commit_id]['source']
        physical_qty = 0
        settlement = self.__settled[time_delivery][commit_id]['record']
        # For extra consumption by buyer greater than settled amount:
        physical_record = settlement.copy()

        # extra purchase by buyer
        # buyer settled for more than consumed
        if extra_purchase:
            print('-extra---------')
            print(buyer_id, extra_purchase)
            print(settlement)
            print(self.__participants[buyer_id]['meter'][time_delivery])

        # extra_purchase and deficit_generation SHOULD be mutually exclusive

        if not extra_purchase and not deficit_generation:
            physical_record.update({
                'time_creation': time_delivery[0],
                'time_consumption': time_delivery[1],
            })
            physical_qty = physical_record['quantity']
            self.__participants[seller_id]['meter'][time_delivery][
                'generation'][energy_source] -= physical_qty
            self.__participants[buyer_id]['meter'][time_delivery][
                'consumption']['other']['external'] -= physical_qty
            physical_transactions.append(physical_record)
        # settled for more than consumed
        elif extra_purchase:
            physical_record.update({
                'quantity': physical_record['quantity'] - extra_purchase,
                'time_creation': time_delivery[0],
                'time_consumption': time_delivery[1],
            })
            financial_record = settlement.copy()
            financial_record.update({
                'quantity': extra_purchase,
                'time_creation': time_delivery[0]
            })
            financial_transactions.append(financial_record)

            if physical_record['quantity']:
                physical_qty = physical_record['quantity']
                self.__participants[seller_id]['meter'][time_delivery][
                    'generation'][energy_source] -= physical_qty
                self.__participants[buyer_id]['meter'][time_delivery][
                    'consumption']['other']['external'] -= physical_qty
                physical_transactions.append(physical_record)

        elif deficit_generation:
            # print('-=-------------=-')
            # print(settlement)
            # print(short)
            # print(self.__participants[seller_id]['meter']['generation']['bess'])

            # seller makes up for less than promised by
            # first, compensate from battery (if extra discharge). These are physical
            # second, financially compensate by buying energy from grid for buyer. These are financial.

            # battery can only compensate for non-dispatch settlements for now
            source_type = await self.__classify_source(
                settlement['energy_source'])
            if source_type == 'non_dispatch':
                residual_bess = self.__participants[seller_id]['meter'][
                    time_delivery]['generation']['bess']
                bess_compensation = min(deficit_generation, residual_bess)
                # print(deficit_generation,
                #       bess_compensation,
                #       self.__participants[seller_id]['meter'][time_delivery]['generation']['bess'],
                #       self.__participants[seller_id]['meter'][time_delivery]['generation']['solar'],
                #       physical_qty)

                if bess_compensation > 0:
                    compensation_record = {
                        'quantity': bess_compensation,
                        'seller_id': settlement['seller_id'],
                        'buyer_id': settlement['buyer_id'],
                        'energy_source': 'bess',
                        'settlement_price': settlement['settlement_price'],
                        'time_creation': time_delivery[0],
                        'time_purchase': settlement['time_purchase'],
                        'time_consumption': time_delivery[1]
                    }
                    self.__participants[seller_id]['meter'][time_delivery][
                        'generation']['bess'] -= bess_compensation
                    self.__participants[buyer_id]['meter'][time_delivery][
                        'consumption']['other'][
                            'external'] -= bess_compensation
                    deficit_generation -= bess_compensation
                    physical_transactions.append(compensation_record)

                    # print(deficit_generation,
                    #       bess_compensation,
                    #       self.__participants[seller_id]['meter'][time_delivery]['generation']['bess'],
                    #       self.__participants[seller_id]['meter'][time_delivery]['generation']['solar'],
                    #       physical_qty)

            # if deficit_generation:
            #     # print(extra_purchase, deficit_generation)
            #     print('-short---------')
            #     # print(buyer_id, extra_purchase)
            #     print(seller_id, deficit_generation)
            #     print(settlement)
            #     print(self.__participants[seller_id]['meter'][time_delivery])

            if deficit_generation > 0:
                financial_record = {
                    'quantity': deficit_generation,
                    'seller_id': seller_id,
                    'buyer_id': buyer_id,
                    'energy_source': 'grid',
                    'settlement_price': -self.__grid.buy_price(),
                    'time_creation': time_delivery[0],
                    'time_purchase': time_delivery[1]
                }
                financial_transactions.append(financial_record)

        await self.__complete_settlement(time_delivery, commit_id)
        return physical_transactions, financial_transactions

    # async def __complete_settlement_cb(self, time_delivery, commit_id):
    #     if not commit_id:
    #         return
    #     time_delivery = tuple(time_delivery)
    #     if time_delivery not in self.__settled:
    #         return
    #     if commit_id not in self.__settled[time_delivery]:
    #         return
    #     del self.__settled[time_delivery][commit_id]

    # mark completion of successful settlements
    async def __complete_settlement(self, time_delivery, commit_id):
        # message = {
        #     'time_delivery': time_delivery,
        #     'commit_id': commit_id,
        #     'seller_id': self.__settled[time_delivery][commit_id]['seller_id'],
        #     'buyer_id': self.__settled[time_delivery][commit_id]['buyer_id']
        # }
        # await self.__client.emit('settlement_complete', message, namespace='/market', callback=self.__complete_settlement_cb)
        # await self.__client.emit('settlement_complete', message, namespace='/market')
        del self.__settled[time_delivery][commit_id]

    @tenacity.retry(wait=tenacity.wait_fixed(5))
    async def __ensure_transactions_complete(self):
        table_len = db_utils.get_table_len(self.__db['path'],
                                           self.__db['table'])
        if table_len < self.transactions_count:
            raise Exception
        return True

    async def record_transactions(self,
                                  buf_len=0,
                                  delay=True,
                                  check_table_len=False):
        """This function records the transaction records into the ledger

        """

        if check_table_len:
            table_len = db_utils.get_table_len(self.__db['path'],
                                               self.__db['table'])
            if table_len < self.transactions_count:
                return False

        if delay and buf_len:
            delay = buf_len / 100
            ts = datetime.datetime.now().timestamp()
            if ts - self.__transaction_last_record_time < delay:
                return False

        transactions_len = len(self.__transactions)
        if transactions_len < buf_len:
            return False

        transactions = self.__transactions[:transactions_len]
        asyncio.create_task(
            db_utils.dump_data(transactions, self.__db['path'],
                               self.__db['table']))

        self.__transaction_last_record_time = datetime.datetime.now(
        ).timestamp()
        del self.__transactions[:transactions_len]
        self.transactions_count += transactions_len
        return True

    async def __clean_market(self, time_delivery):
        # clean buffer from 2 rounds before the current round
        # ensure this will not interfere with settlement callbacks
        duration = self.__timing['duration']
        time_clean = (time_delivery[0] - duration, time_delivery[1] - duration)
        self.__open['dispatch'].pop(time_clean, None)
        self.__open['non_dispatch'].pop(time_clean, None)
        self.__settled.pop(time_clean, None)
        for participant in self.__participants:
            self.__participants[participant]['meter'].pop(time_delivery, None)

    async def __update_time(self, time):
        self.__server_ts = time['time']
        duration = time['duration']
        start_time = time['time']
        end_time = start_time + duration
        self.__timing.update({
            'timezone':
            self.__timing['timezone'],
            'duration':
            duration,
            'last_round':
            self.__timing['current_round'],
            'current_round': (start_time, end_time),
            'last_settle':
            (start_time + duration * (self.__timing['close_steps'] - 1),
             start_time + duration * self.__timing['close_steps']),
            'next_settle':
            (start_time + duration * self.__timing['close_steps'],
             start_time + duration * (self.__timing['close_steps'] + 1))
        })

    # Make sure time interval provided is valid
    async def __time_interval_is_valid(self, time_interval: tuple):
        duration = self.__timing['duration']
        if (time_interval[1] - time_interval[0]) % duration != 0:
            # make sure duration is a multiple of round duration
            return False
        if time_interval[0] % duration != 0:
            return False
        if time_interval[1] % duration != 0:
            return False
        return True

    async def __match_all(self, time_delivery):
        for source_type in {'dispatch', 'non_dispatch'}:
            # important: dispatch must be first!!!
            await self.__match(source_type, time_delivery)
        self.__status['round_matched'] = True

    # should be for simulation mode only
    @tenacity.retry(wait=tenacity.wait_fixed(0.01))
    async def __ensure_round_complete(self):
        if self.__status['round_metered'] < self.__status[
                'active_participants']:
            raise Exception

        if not self.__status['round_matched']:
            raise Exception

        if set(self.__status['round_settle_delivered']) != set(
                self.__status['round_settled']):
            raise Exception

    # Finish all processes and remove all unnecessary/ remaining records in preparation for a new time step, begin processes for next step
    async def step(self, timeout=60, sim_params=None):
        # timing for simulation mode and real-time mode a slightly different due to one with an explicit end condition. RT mode sequence is not too relevant at the moment will be added later.
        if self.__timing['mode'] == 'sim':
            await self.__update_time(sim_params)
            if not self.__timing['current_round'][0] % 3600:
                self.__grid.update_price(self.__timing['current_round'][0],
                                         self.__timing['timezone'])
            await self.__start_round(duration=timeout)
            await self.__match_all(self.__timing['last_settle'])
            await self.__ensure_round_complete()
            await self.__process_energy_exchange(self.__timing['current_round']
                                                 )
            await self.__clean_market(self.__timing['last_round'])
            await self.__client.emit('end_round',
                                     data='',
                                     namespace='/simulation')

    async def loop(self):
        # change loop depending on sim mode or RT mode
        while self.run:
            if self.server_online and self.__timing['mode'] == 'rt':
                await self.step(60)
                continue
            await self.__client.sleep(0.001)

        await self.__client.sleep(5)
        raise SystemExit

    async def reset_market(self):
        self.__db.clear()
        self.transactions_count = 0
        self.__open['non_dispatch'].clear()
        self.__open['dispatch'].clear()
        self.__settled.clear()
        for participant in self.__participants:
            self.__participants[participant]['meter'].clear()

    async def end_sim_generation(self):
        await self.record_transactions(delay=False)
        await self.__ensure_transactions_complete()
        await self.reset_market()
        await self.__client.emit('market_ready', namespace='/simulation')