import asyncio
import json
import requests
import uuid

import tornado

from .commands import TRADER_UPDATE_PING, CLIENTS_ANSWER, CLIENTS_ACCEPT, CLIENTS_DECLINE, IB_MAKE_CALL, \
    IB_ANSWER_CALL, IB_HIT_CALL, IB_NTG_CALL, DP_ADD, DP_CANCEL, DP_CANCEL_ALL, EB_HIT, EB_ADD, EB_CANCEL, \
    EB_CANCEL_ALL
from .events import ROOM_PAUSED, ROOM_RESUMED, ROOM_FINISHED, ROOM_PARAM_UPDATED, CLIENTS_CALL_CREATED, \
    CLIENTS_CALL_ACCEPTED, CLIENTS_CALL_DECLINED, CLIENTS_CALL_EXPIRED, IB_CALL_CREATED, IB_CALL_ANSWERED, \
    IB_CALL_HIT, IB_CALL_NTG, IB_CALL_EXPIRED, DP_ORDER_ADDED, DP_ORDER_CANCELLED, DP_ORDER_EXECUTED, EB_ORDER_ADDED, \
    EB_ORDER_CANCELLED, EB_ORDER_EXECUTED, EB_DEPTH_CHANGED, MACRO_EVENT_PUBLISHED, MARKET_PRICE_CHANGED, \
    MARKET_PRICE_CHART_UPDATED, POSITION_UPDATED, PNL_UPDATED

from .exceptions import LoginError, BadRequest, ServerError
from .websocket import WebSocket


IS_PRODUCTION = True

if IS_PRODUCTION:
    AUTH_URL = 'https://dashboard.sim.traderion.com/api/auth/get_token/'
    TRADING_SERVICE_URL = 'https://trading-proxy.sim.traderion.com/'
    TRADING_SERVICE_SOCKET_URL = 'wss://trading-proxy.sim.traderion.com/ws/'
else:
    AUTH_URL = 'http://localhost:8000/api/auth/get_token/'
    TRADING_SERVICE_URL = 'http://localhost:8001/'
    TRADING_SERVICE_SOCKET_URL = 'ws://localhost:8001/ws/'


BID, ASK = (0, 1)

ROOM_STATUS = {
    0: 'pending',
    1: 'playing',
    2: 'paused',
    3: 'finished'
}

IB_CALL_HIT_BID, IB_CALL_HIT_ASK = (3, 4)

DP_ORDER_TYPE = {
    0: 'limit',
    1: 'market'
}

EB_ORDER_TYPE = {
    0: 'limit',
    1: 'iceberg',
    2: 'limit_plus',
    3: 'iceberg_plus'
}

ORDER_STATUS = {
    0: 'pending',
    1: 'completed',
    2: 'cancelled'
}


class TraderionClient(object):
    """
    Main API interface.
    """

    @staticmethod
    def _create_event_dispatcher(obj):
        event_dispatcher_map = {
            # Room events
            ROOM_PAUSED: '_on_room_status_changed',
            ROOM_RESUMED: '_on_room_status_changed',
            ROOM_FINISHED: '_on_room_status_changed',

            # Room parameters events
            ROOM_PARAM_UPDATED: '_on_room_param_updated',

            # Client call events
            CLIENTS_CALL_CREATED: '_on_clients_call_created',
            CLIENTS_CALL_ACCEPTED: '_on_clients_call_accepted',
            CLIENTS_CALL_DECLINED: '_on_clients_call_declined',
            CLIENTS_CALL_EXPIRED: '_on_clients_call_expired',

            # IB call events
            IB_CALL_CREATED: '_on_ib_call_created',
            IB_CALL_ANSWERED: '_on_ib_call_answered',
            IB_CALL_HIT: '_on_ib_call_hit',
            IB_CALL_NTG: '_on_ib_call_ntg',
            IB_CALL_EXPIRED: '_on_ib_call_expired',

            # Dark pool events
            DP_ORDER_ADDED: '_on_dp_order_added',
            DP_ORDER_CANCELLED: '_on_dp_order_cancelled',
            DP_ORDER_EXECUTED: '_on_dp_order_executed',

            # Electronic broker events
            EB_ORDER_ADDED: '_on_eb_order_added',
            EB_ORDER_CANCELLED: '_on_eb_order_cancelled',
            EB_ORDER_EXECUTED: '_on_eb_order_executed',
            EB_DEPTH_CHANGED: '_on_eb_depth_changed',

            # Macro event events
            MACRO_EVENT_PUBLISHED: '_on_macro_event_published',

            # Market events
            MARKET_PRICE_CHANGED: '_on_market_price_changed',
            MARKET_PRICE_CHART_UPDATED: '_on_market_price_chart_updated',

            # Position events
            POSITION_UPDATED: '_on_position_updated',
            PNL_UPDATED: '_on_pnl_updated',
        }

        def dispatcher(event, data):
            if event in event_dispatcher_map:
                getattr(obj, event_dispatcher_map[event])(data)

        return dispatcher

    def __init__(self, username, password, room_id, bot):
        """
        Initialize Traderion client.

        On instantiation, the client will authenticate with the provided credentials. You can then use this client to
        send commands (e.g. add an electronic broker order) or get information about the game state (e.g. get the market
        price at some point).

        :param str username: traderion username
        :param str password: traderion password
        :param str room_id: id of the current playing room
        :param bot: bot associated with this client
        :type bot: :class:`.TraderionBot`

        :raises LoginError: if the credentials are not correct
        :raises BadRequest: this most likely means a non-existent room id
        :raises ServerError: if some other error occured on the server
        """
        self.room_id = str(room_id)
        self.trader = username
        self.bot = bot

        self.ws = None
        self.room_finished_future = None
        self.event_dispatcher = self._create_event_dispatcher(self)

        self.room_status = None
        self.swifts = None
        self.room_params = None

        self.eb_orders = None
        self.eb_depth = None
        self.market_prices = None
        self.price_curves = None
        self.positions = None

        self.trader_ping = tornado.ioloop.PeriodicCallback(self._send_trader_ping, 5000)
        self.bot_main_loop = tornado.ioloop.PeriodicCallback(self._call_bot_main_loop, self.bot.loop_sleep * 1000)

        login_token = self._login(username, password)
        self.token = self._init_session(login_token)
        self._connect_to_service_socket()

    def get_room_status(self):
        """
        Returns the room status.

        :return: one of ``'pending'`` | ``'playing'`` | ``'paused'`` | ``'finished'``
        :rtype: str
        """
        return self.room_status

    def get_swifts(self):
        """
        Returns the swifts for this scenario.

        :return: a list of swift objects
        :rtype: list

        **Swift object structure:**
            * **swift_id** (*int*): swift id
            * **code** (*str*): swift code (e.g. ``'EUR/USD'``)
            * **name** (*str*): swift name (e.g. ``'EUR/USD'``)
            * **asset_class** (*str*): one of ``'FX'`` | ``'EQ'`` | ``'FI'``
            * **reporting_currency** (*str*): currency used for reporting PnL (e.g. ``'USD'``)
            * **basis_point** (*str*): basis point as string (e.g. ``'0.0001'``)
              (is always equal to 10^(-*decimal_places*))
            * **decimal_places** (*int*): number of decimals used when reporting the price
            * **tick** (*int*): minimum upward or downward movement in the price
            * **ticket_unit** (*int*): smallest amount that can be traded (all amounts are multiples of this amount)
            * **extra** (*dict*): dictionary with extra information for ``'FI'``:
                * **issue_date**
                * **years**
                * **frequency**
                * **coupon**
                * **face_value**
        """
        return self.swifts

    def get_room_params(self):
        """
        Return the room parameters for this scenario.

        :return: room parameters as a dictionary
        :rtype: dict

        **Room parameters dictionary structure:** ::

            {
                'general': {                 # general parameters
                    'param1': value
                    ...
                },
                'swift_id1': {               # swift1 parameters
                    'swift1_param1': value
                    ...
                }
                'swift_id2': {               # swift2 parameters
                    'swift1_param1': value
                    ...
                }
                ...
            }

        **General parameters:**
            * IB_CALLS_ENABLED: 0/1 (False/True)
            * IB_CALLS_SECONDS: number of seconds after which an IB call expires
            * CLIENT_CALLS_ENABLED: 0/1 (False/True)
            * CLIENT_CALL_SECONDS: number of seconds after which a client call expires
            * IMPACT_VOTE_SECONDS: duration of an impact vote (in seconds)

        **Per swift parameters**:
            * PLAYERS_MIN_TICKET: minimum amount a player can trade on a single deal
            * PLAYER_MAX_TICKET: maximum amount a player can trade on a single deal
            * PLAYERS_POSITION_LIMIT: maximum position a player should have at any point
            * PLAYERS_IB_MAX_SPREAD: maximum spread on IB calls quotes
        """
        return self.room_params

    def get_dp_orders(self, swift_id=None, direction=None):
        """
        Returns your current (pending) DP (dark pool) orders.

        If *swift_id* and *direction* are not specified, orders for all swifts and both directions (``BID`` and ``ASK``)
        are returned. If only the *swift_id* is specified, orders for both directions are returned for that swift.
        If *direction* is also specified, only the orders for that direction (and that swift) are returned.

        :param swift_id: swift id (default is *None*)
        :type swift_id: None or int
        :param direction: *None* or one of 0/1 (``BID``/``ASK``) (default is *None*). Must be *None* if *swift_id* is
                          *None*.
        :type direction: None or int
        :return: the requested DP orders
        :rtype: dict

        **Returned object structure:**
            1. *swift_id* is specified, *direction* is specified: ::

                    list of DP orders

            2. *swift_id* is specified, *direction* is *None* ::

                {
                    BID: list of DP orders,
                    ASK: list of DP orders
                }

            3. *swift_id* is *None*, *direction* is *None* ::

                {
                    swift_id1: {
                        BID: list of DP orders,
                        ASK: list of DP orders
                    },
                    swift_id2: {
                        BID: list of DP orders,
                        ASK: list of DP orders
                    }
                    ...
                }

        **DP order object structure:**
            * **id** (*int*): order id
            * **room_time** (*int*): room time when the order was added
            * **swift_id** (*int*): swift id
            * **order_type** (*str*): one of ``'limit'`` | ``'market'``
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (you buy/you sell) order direction
            * **amount** (*int*): order amount
            * **price** (*None* or *float*): order price for limit orders. For market orders, this is always *None*.
            * **status** (*str*): one of ``'pending'`` | ``'completed'`` | ``'cancelled'``
        """
        assert not (swift_id is None and direction is not None)

        if swift_id is None:
            return self.dp_orders
        elif direction is None:
            return self.dp_orders[swift_id]
        return self.dp_orders[swift_id][direction]

    def get_eb_orders(self, swift_id=None, direction=None):
        """
        Returns your current (pending) EB orders.

        If *swift_id* and *direction* are not specified, orders for all swifts and both directions (``BID`` and ``ASK``)
        are returned. If only the *swift_id* is specified, orders for both directions are returned for that swift.
        If *direction* is also specified, only the orders for that direction (and that swift) are returned.

        :param swift_id: swift id (default is *None*)
        :type swift_id: None or int
        :param direction: *None* or one of 0/1 (``BID``/``ASK``) (default is *None*). Must be *None* if *swift_id* is
                          *None*.
        :type direction: None or int
        :return: the requested EB orders
        :rtype: dict

        **Returned object structure:**
            1. *swift_id* is specified, *direction* is specified: ::

                    list of EB orders

            2. *swift_id* is specified, *direction* is *None* ::

                {
                    BID: list of EB orders,
                    ASK: list of EB orders
                }

            3. *swift_id* is *None*, *direction* is *None* ::

                {
                    swift_id1: {
                        BID: list of EB orders,
                        ASK: list of EB orders
                    },
                    swift_id2: {
                        BID: list of EB orders,
                        ASK: list of EB orders
                    }
                    ...
                }

        **EB order object structure:**
            * **id** (*int*): order id
            * **room_time** (*int*): room time when the order was added
            * **swift_id** (*int*): swift id
            * **order_type** (*str*): one of ``'limit'`` | ``'iceberg'`` | ``'limit_plus'`` | ``'iceberg_plus'``
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (you buy/you sell) order direction
            * **amount** (*int*): order amount
            * **chunk** (*None* or *int*): visible chunk for an Iceberg order. Is *None* if the order is not an Iceberg
                                           one.
            * **visible_amount** (*int*): the visible amount of the order. For Limit orders, this is equal to the order
                                          *amount*. For Iceberg orders, this will always be ``min(amount, chunk)``.
            * **price** (*float*): order price
            * **status** (*str*): one of ``'pending'`` | ``'completed'`` | ``'cancelled'``
        """
        assert not (swift_id is None and direction is not None)

        if swift_id is None:
            return self.eb_orders
        elif direction is None:
            return self.eb_orders[swift_id]
        return self.eb_orders[swift_id][direction]

    def get_eb_depth(self, swift_id=None, direction=None):
        """
        Returns the current electronic broker depth.

        If *swift_id* and *direction* are not specified, depths for all swifts and both directions (``BID`` and
        ``ASK``) are returned. If only the *swift_id* is specified, depths for both directions are returned for that
        swift. If *direction* is also specified, only the depth for that direction (and that swift) is returned.

        Be mindful that your own orders also appear in the EB depth. In the case of Iceberg orders, only the visible
        amount of the order will be considered in the EB depth.

        :param swift_id: swift id (default is *None*)
        :type swift_id: None or int
        :param direction: *None* or one of 0/1 (``BID``/``ASK``) (default is *None*). Must be *None* if swift is *None*.
        :type direction: None or int
        :return: the requested EB depths
        :rtype: dict

        **Returned object structure:**
            1. *swift_id* is specified, *direction* is specified ::

                an EB depth object

            2. *swift_id* is specified, *direction* is *None* ::

                {
                    BID: EB depth object,
                    ASK: EB depth object
                }

            3. *swift_id* is *None*, *direction* is *None* ::

                {
                    swift_id1: {
                        BID: EB depth object,
                        ASK: EB depth object
                    },
                    swift_id2: {
                        BID: EB depth object,
                        ASK: EB depth object
                    }
                    ...
                }

        **EB depth object structure:**
            list of EB depth orders (dictionaries) with the structure:
                * **count** (*int*): the number of atomic orders at this price
                * **amount** (*int*): total amount at this price
                * **price** (*float*): price

            *Note*: An EB depth order can be made up of several atomic orders (having the same price). A price appears
            only once in the depth and the amounts of all the atomic orders at that price are summed up. Prices
            are decreasing for ``BID`` depth and increasing for ``ASK`` depth.
        """
        assert not (swift_id is None and direction is not None)

        if swift_id is None:
            return self.eb_depth
        elif direction is None:
            return self.eb_depth[swift_id]
        return self.eb_depth[swift_id][direction]

    def get_market_prices(self, swift_id=None):
        """
        Returns the current market prices.

        If *swift_id* is not specified (*None*), a dictionary containing market prices for all swifts is returned
        (keys are swift ids and values are market price objects). Otherwise, the market price object for the specified
        swift is returned.

        :param swift_id: swift id (default is *None*)
        :type swift_id: None or int
        :return: if *swift_id* is *None*, market prices for all swifts. Otherwise, the market price for the specified
                 swift.
        :rtype: dict

        **Returned object structure:**
            1. *swift_id* is specified ::

                a market price object

            2. *swift_id* is *None* ::

                {
                    swift_id1: market price object for swift1,
                    swift_id2: market price object for swift2,
                    ...
                }

        **Market price object structure:**
            * **swift_id** (*int*): swift id
            * **open** (*float*): the open price of the scenario
            * **bid** (*float*): market bid price
            * **ask** (*float*): market ask price
            * **price** (*float*): market mid price (average of *bid* and *ask*)
        """
        if swift_id is None:
            return self.market_prices
        return self.market_prices[swift_id]

    def get_price_curves(self, swift_id=None):
        """
        Returns the current price curves.

        If *swift_id* is not specified (*None*), a dictionary containing price curves for all swifts is returned (keys
        are swift ids and values are price curve objects). Otherwise, the price curve object for the specified swift is
        returned.

        :param swift_id: swift id (default is *None*)
        :type swift_id: None or int
        :return: if *swift_id* is *None*, price curves for all swifts. Otherwise, the price curve for the specified
                 swift.
        :rtype: list or dict

        **Returned object structure:**
            1. *swift_id* is specified ::

                a price curve

            2. *swift_id* is *None* ::

                {
                    swift_id1: price curve for swift1,
                    swift_id2: price curve for swift2,
                    ...
                }

        **Structure of a price curve:**
            list of price curve points (dictionaries) with structure:
                * **swift_id** (*int*): swift id
                * **room_time** (*int*): room time when the price was reported
                * **price** (*float*): reported mid price (average of *bid* and *ask* at *room_time*)
        """
        if swift_id is None:
            return self.price_curves
        return self.price_curves[swift_id]

    def get_positions(self, swift_id=None):
        """
        Returns your current positions.

        If *swift_id* is not specified (*None*), a dictionary containing positions for all swifts is returned (keys are
        swift ids and values are position objects). Otherwise, the position for the specified swift is returned.

        :param int swift_id: swift id (default is *None*)
        :return: if *swift_id* is *None*, positions for all swifts. Otherwise, the position for the specified swift.
        :rtype: dict

        **Returned object structure:**
            1. *swift_id* is specified ::

                a position object

            2. *swift_id* is *None* ::

                {
                    swift_id1: position object for swift1,
                    swift_id2: position object for swift2,
                    ...
                }

        **Position object structure:**
            * **swift_id** (*int*): swift id
            * **amount** (*int*): amount of the position
            * **rate** (*float*): average rate of the position
            * **pnl** (*int*): the PnL expressed in the reporting currency of the swift
            * **converted_pnl** (*int*): the PnL expressed in USD
            * **pnl_percentage** (*float*): the PnL relative to the position limit
            * **return_on_volume** (*float*): the PnL relative to the total traded volume
            * **max_position** (*int*): maximum position (amount) you had since the beginning of the game
            * **total_volume** (*int*): total traded volume
            * **yield** (*float* or *None*): bond yield (*None* if the asset type is not ``FI``)
            * **dv01** (*int* or *None*): bond DV01 (*None* if the asset type is not ``FI``)
            * **day_change** (*float*): difference between the current market price and the open price
        """
        if swift_id is None:
            return self.positions
        return self.positions[swift_id]

    async def quote_client_call(self, call_id, quote):
        """
        Call this method to quote a client call.

        The *client_price* of this call must be *None*. If this operation is successful, you should expect the client
        to either accept or reject the offer, which means one of
        :func:`TraderionBot.on_client_call_accepted <traderion2.bot.TraderionBot.on_client_call_accepted>`
        or :func:`TraderionBot.on_client_call_declined <traderion2.bot.TraderionBot.on_client_call_declined>`
        will get triggered.

        :param int call_id: call id
        :param float quote: the price offered to the client
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'call_id': call_id,
            'quote': quote
        }
        return await self.ws.send_command(CLIENTS_ANSWER, payload_body)

    async def accept_client_call(self, call_id):
        """
        Call this method to accept a client call.

        The *client_price* of this call must **not** be *None*.

        :param int call_id: call id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'call_id': call_id
        }
        return await self.ws.send_command(CLIENTS_ACCEPT, payload_body)

    async def decline_client_call(self, call_id):
        """
        Call this method to decline a client call.

        The *client_price* of this call must **not** be *None*.

        :param int call_id: call id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'call_id': call_id
        }
        return await self.ws.send_command(CLIENTS_DECLINE, payload_body)

    async def make_ib_call(self, swift_id, amount):
        """
        Call this method to make an IB call.

        If this operation is successful, you should expect other banks to respond to your call, which means
        :func:`TraderionBot.on_ib_call_answered <traderion2.bot.TraderionBot.on_ib_call_answered>` will get
        called (once for every offer you receive).

        *Note*: this method will return an error if you have unanswered offers from previous ib calls you made.

        :param int swift_id: swift id
        :param int amount: amount you want to buy/sell
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'swift_id': swift_id,
            'amount': amount
        }
        return await self.ws.send_command(IB_MAKE_CALL, payload_body)

    async def quote_ib_call(self, call_id, bid, ask):
        """
        Call this method to quote an IB call.

        If this operation is successful, you should expect the client bank to either hit the offer (on bid or ask) or do
        nothing, which means one of :func:`TraderionBot.on_ib_call_hit <traderion2.bot.TraderionBot.on_ib_call_hit>` or
        :func:`TraderionBot.on_ib_call_ntg <traderion2.bot.TraderionBot.on_ib_call_ntg>` will get triggered.

        :param int call_id: call id
        :param float bid: your bid price
        :param float ask: your ask price
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'call_id': call_id,
            'bid': bid,
            'ask': ask
        }
        return await self.ws.send_command(IB_ANSWER_CALL, payload_body)

    async def hit_ib_call(self, call_id, direction):
        """
        Call this method to hit the bid or ask price of an answered IB outgoing call.

        This method should be called only after the IB call was answered (quoted). The recommended way
        is to call it from :func:`TraderionBot.on_ib_call_answered <traderion2.bot.TraderionBot.on_ib_call_answered>`.

        :param int call_id: call id
        :param int direction: the price you hit: 0/1 (``BID``/``ASK``)
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'call_id': call_id,
            'direction': direction
        }
        return await self.ws.send_command(IB_HIT_CALL, payload_body)

    async def ntg_ib_call(self, call_id):
        """
        Call this method to respond with NTG to an answered outgoing IB call.

        This method should be called only after the IB call was answered (quoted). The recommended way
        is to call it from :func:`TraderionBot.on_ib_call_answered <traderion2.bot.TraderionBot.on_ib_call_answered>`.

        :param int call_id: call id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: None or dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'call_id': call_id
        }
        return await self.ws.send_command(IB_NTG_CALL, payload_body)

    async def dp_add_order(self, swift_id, direction, amount, price=None):
        """
        Call this method to add an order in the dark pool.

        Orders can have 2 types:
            1. **Market orders**: orders that are executed at the market mid price. This is the default.
            2. **Limit orders**: orders that are executed at a price equal or better than the specified price. You can \
            add a limit order by setting the *price* parameter.

        :param int swift_id: swift id
        :param int direction: 0/1 (``BID``/``ASK``) (you buy / you sell) order direction
        :param int amount: order amount
        :param price: order price for a limit order (default is *None*). If *None*, the order will be a market order.
                      Otherwise, it will be a limit order.
        :type price: None or float
        :return: a dictionary with *error* and *data* fields. If no error occured, *error* is *None* and *data* is the
                 order id. Otherwise, *error* will be a dictionary with two string fields: *code* and *detail*.
        :rtype: dict
        :raises: CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'swift_id': swift_id,
            'direction': direction,
            'amount': amount
        }
        if price is not None:
            payload_body['price'] = price

        return await self.ws.send_command(DP_ADD, payload_body, return_data=True)

    async def dp_cancel_order(self, order_id):
        """
        Call this method to cancel a dark pool order.

        :param int order_id: order id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'order_id': order_id
        }
        return await self.ws.send_command(DP_CANCEL, payload_body)

    async def dp_cancel_all_orders(self, swift_id):
        """
        Call this method to cancel all dark pool orders for a swift.

        :param int swift_id: swift id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'swift_id': swift_id
        }
        return await self.ws.send_command(DP_CANCEL_ALL, payload_body)

    async def hit_price(self, swift_id, direction, amount, price=None, fill_or_kill=False):
        """
        Call this method to hit orders in the electronic broker.

        This method will hit all orders with a price equal or better to the specified price up to the specfied amount.
        Orders with a better price are hit first. A better price means higher for BID and lower for ASK.

        :param int swift_id: swift id
        :param int direction: 0/1 (``BID``/``ASK``) direction to hit
        :param int amount: maximum hit amount (you might hit less than this amount in various conditions: for example,
                           when there isn't enough volume at an acceptable price)
        :param price: price to hit. All orders with this price or a better price are considered (better prices
                      are considered first). If unspecified, the best price (market price) for the specified
                      direction is hit.
        :type price: None or float
        :param bool fill_or_kill: whether this is a Fill Or Kill hit (default is *False*). A Fill Or Kill hit will
                                  hit for the whole amount or none of it. In other words, if there isn't enough volume
                                  to fill the specified amount, nothing will happen (no orders will be hit).
        :return: a dictionary with *error* and *data* fields. If no error occured, *error* is *None* and *data* is the
                 amount that was hit. Otherwise, *error* will be a dictionary with two string fields: *code* and
                 *detail*.
        :rtype: dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        if price is None:
            market_price = self.get_market_prices(swift_id)
            price = {BID: market_price['bid'], ASK: market_price['ask']}[direction]

        payload_body = {
            'swift_id': swift_id,
            'direction': direction,
            'amount': amount,
            'price': price,
            'fill_or_kill': fill_or_kill
        }
        return await self.ws.send_command(EB_HIT, payload_body, return_data=True)

    async def eb_add_mo(self, swift_id, direction, amount):
        """
        Add market order in the electronic broker.

        :param int swift_id: swift id
        :param int direction: 0/1 (``BID``/``ASK``) (you buy / you sell) order direction
        :param int amount: order amount
        """
        await self.hit_price(swift_id, direction, amount)

    async def eb_add_ioc(self, swift_id, direction, amount, price):
        """
        Add 'Immediate-or-Cancel' order in the electronic broker.

        :param int swift_id: swift id
        :param int direction: 0/1 (``BID``/``ASK``) (you buy / you sell) order direction
        :param int amount: order amount
        :param float price: order price
        """
        await self.hit_price(swift_id, direction, amount, price)

    async def eb_add_foc(self, swift_id, direction, amount, price):
        """
        Add 'Fill-or-Kill' order in the electronic broker.

        :param int swift_id: swift id
        :param int direction: 0/1 (``BID``/``ASK``) (you buy / you sell) order direction
        :param int amount: order amount
        :param float price: order price
        """
        await self.hit_price(swift_id, direction, amount, price, fill_or_kill=True)


    async def eb_add_order(self, swift_id, direction, amount, price, chunk=None, plus=False):
        """
        Call this method to add an order in the electronic broker.

        Orders can have 4 types:
            1. **Limit orders**: this is the default.
            2. **Iceberg orders**: only a chunk of the full amount is visible in the electronic broker at any time. \
                You can add an Iceberg order by setting the *chunk* parameter. Iceberg orders have an increased ticket \
                limit.
            3. **Limit Plus orders**: limit orders that can be executed in the dark pool as well (possibly at a better \
                price).
            4. **Iceberg Plus orders**: Iceberg orders that can be executed in the dark pool as well (possibly at a \
                better price).

        :param int swift_id: swift id
        :param int direction: 0/1 (``BID``/``ASK``) (you buy / you sell) order direction
        :param int amount: order amount
        :param float price: order price
        :param chunk: visible chunk for an Iceberg order (default is *None*). If *None*, the order will be a limit
                      order. Otherwise, it will be an Iceberg order.
        :type chunk: None or int
        :param bool plus: whether the order should be a Plus order (default is *False*). A Plus order can be executed
                          in the dark pool as well.
        :return: a dictionary with *error* and *data* fields. If no error occured, *error* is *None* and *data* is the
                 order id. Otherwise, *error* will be a dictionary with two string fields: *code* and *detail*.
        :rtype: dict
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'swift_id': swift_id,
            'direction': direction,
            'amount': amount,
            'price': price,
            'plus': plus
        }
        if chunk is not None:
            payload_body['chunk'] = chunk

        return await self.ws.send_command(EB_ADD, payload_body, return_data=True)

    async def eb_cancel_order(self, order_id):
        """
        Call this method to cancel an electronic broker order.

        :param int order_id: order id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'order_id': order_id
        }
        return await self.ws.send_command(EB_CANCEL, payload_body)

    async def eb_cancel_all_orders(self, swift_id):
        """
        Call this coroutine to cancel all electronic broker orders for a swift.

        :param int swift_id: swift id
        :return: *None* if no error occured. Otherwise, an error as a dictionary with two string fields: *code* and
                 *detail*.
        :raises CommandTimeoutError: if the server took too long to respond
        """
        payload_body = {
            'swift_id': swift_id
        }
        return await self.ws.send_command(EB_CANCEL_ALL, payload_body)

    async def _run_until_room_finished(self):
        if self.room_status != 'finished':
            self.room_finished_future = asyncio.Future()
            await self.room_finished_future

    def _stop_early(self):
        # The room is not really finished, but we act like it is, since we need to stop the bot early.
        self._on_room_finished()

    def _send_trader_ping(self):
        asyncio.ensure_future(self.ws.send_command(TRADER_UPDATE_PING))

    def _call_bot_main_loop(self):
        if self.room_status == 'playing':
            asyncio.ensure_future(self.bot.main_loop())

    def _login(self, username, password):
        response = requests.post(AUTH_URL, data={'username': username, 'password': password})

        if response.status_code != 200:
            raise LoginError(response.text)

        return response.json()['token']

    def _init_session(self, login_token):
        data = {'room_id': self.room_id}
        headers = {
            'Origin': TRADING_SERVICE_URL,
            'Authorization': 'JWT ' + login_token
        }
        response = requests.post(TRADING_SERVICE_URL + 'init_session/', json=data, headers=headers)

        if response.status_code == 400:
            raise BadRequest(response.json()['detail'])
        elif response.status_code != 200:
            raise ServerError(response.text)

        response = response.json()
        self._fill_initial_data(response['state']['data'])

        return response['token']

    def _fill_initial_data(self, init_data):
        self.room_status = ROOM_STATUS[init_data['room']['status']]
        self.swifts = init_data['swifts']
        self.room_params = init_data['settings']

        self.dp_orders = {}
        for swift_id, swift_dp_orders in init_data['dp_orders'].items():
            self.dp_orders[int(swift_id)] = {}
            for direction, dp_orders in swift_dp_orders.items():
                orders = [self._serialize_dp_order(order) for order in dp_orders]
                self.dp_orders[int(swift_id)][int(direction)] = orders

        self.eb_orders = {}
        for swift_id, swift_eb_orders in init_data['eb_orders'].items():
            self.eb_orders[int(swift_id)] = {}
            for direction, eb_orders in swift_eb_orders.items():
                orders = [self._serialize_eb_order(order) for order in eb_orders]
                self.eb_orders[int(swift_id)][int(direction)] = orders

        self.eb_depth = {}
        for swift_id, swift_eb_depths in init_data['eb_depth'].items():
            self.eb_depth[int(swift_id)] = {}
            for direction, eb_depth in swift_eb_depths.items():
                self.eb_depth[int(swift_id)][int(direction)] = self._serialize_eb_depth(eb_depth)

        self.market_prices = {}
        for swift_id, market_price in init_data['market_prices'].items():
            self.market_prices[int(swift_id)] = self._serialize_market_price(market_price)

        self.price_curves = {}
        for swift_id, price_curve in init_data['price_curves'].items():
            price_curve = [self._serialize_price_curve_price(price) for price in price_curve]
            self.price_curves[int(swift_id)] = price_curve

        self.positions = {}
        for swift_id, position in init_data['positions'].items():
            self.positions[int(swift_id)] = self._serialize_position(position)

    def _connect_to_service_socket(self):
        url = '%s?token=%s' % (TRADING_SERVICE_SOCKET_URL, self.token)
        self.ws = WebSocket(url, on_open_callback=self._on_socket_open, on_message_callback=self._on_socket_message)

    def _on_socket_open(self):
        self.trader_ping.start()
        self.bot_main_loop.start()

        # Tornado's PeriodicCallback executes first after callback_time miliseconds, but we want the main
        # loop and trader ping to be called immediately.
        self._send_trader_ping()
        self._call_bot_main_loop()

    def _on_socket_message(self, message):
        event = message['headers']['event']
        self.event_dispatcher(event, message['body'])

    def _on_room_status_changed(self, data):
        old_status = self.room_status
        self.room_status = ROOM_STATUS[data['status']]

        # on_room_status_change is not a coroutine, so we can make sure that it is
        # always called when the room is finished. Otherwise, we might stop the main loop
        # before this callback gets executed.
        self.bot.on_room_status_change(old_status, self.room_status)

        if self.room_status == 'finished':
            self._on_room_finished()

    def _on_room_finished(self):
        self.trader_ping.stop()
        self.bot_main_loop.stop()
        self.ws.close()
        if self.room_finished_future:
            self.room_finished_future.set_result(None)

    def _on_room_param_updated(self, data):
        param_data = data['param']

        category = str(param_data['swift_id'] or 'general')
        name = param_data['code']
        if name not in self.room_params[category]:
            return

        value = param_data['value']
        old_value = self.room_params[category][name]
        self.room_params[category][name] = value

        asyncio.ensure_future(self.bot.on_room_param_change(category, name, old_value, value))

    def _on_clients_call_created(self, data):
        call = self._serialize_client_call(data['call'])
        asyncio.ensure_future(self.bot.on_new_client_call(call))

    def _on_clients_call_accepted(self, data):
        call = self._serialize_client_call(data['call'])

        # We get a CLIENTS_CALL_ACCEPTED event even when we accept a fixed price. Don't notify the user for those.
        if call['trader_quote'] is not None:
            asyncio.ensure_future(self.bot.on_client_call_accepted(call))

    def _on_clients_call_declined(self, data):
        call = self._serialize_client_call(data['call'])

        # We get a CLIENTS_CALL_DECLINED event even when we decline a fixed price. Don't notify the user for those.
        if call['trader_quote'] is not None:
            asyncio.ensure_future(self.bot.on_client_call_declined(call))

    def _on_clients_call_expired(self, data):
        call = self._serialize_client_call(data['call'])
        asyncio.ensure_future(self.bot.on_client_call_expired(call))

    def _on_ib_call_created(self, data):
        call = self._serialize_ib_call(data['call'])

        # Notify the user only on incoming calls
        if call['market_maker'] == self.trader:
            asyncio.ensure_future(self.bot.on_new_ib_call(call))

    def _on_ib_call_answered(self, data):
        call = self._serialize_ib_call(data['call'])

        # Notify the user only on answers to his own outgoing calls
        if call['market_taker'] == self.trader:
            asyncio.ensure_future(self.bot.on_ib_call_answered(call))

    def _on_ib_call_hit(self, data):
        call = self._serialize_ib_call(data['call'])

        # Notify the user only when hits happen on his own offers
        if call['market_maker'] == self.trader:
            asyncio.ensure_future(self.bot.on_ib_call_hit(call))

    def _on_ib_call_ntg(self, data):
        call = self._serialize_ib_call(data['call'])

        # Notify the user only when NTG happens on his own offers
        if call['market_maker'] == self.trader:
            asyncio.ensure_future(self.bot.on_ib_call_ntg(call))

    def _on_ib_call_expired(self, data):
        call = self._serialize_ib_call(data['call'])
        asyncio.ensure_future(self.bot.on_ib_call_expired(call))

    def _on_dp_order_added(self, data):
        order = self._serialize_dp_order(data['order'])
        swift_id = order['swift_id']
        direction = order['direction']

        self.dp_orders[swift_id][direction].append(order)

    def _on_dp_order_cancelled(self, data):
        order = self._serialize_dp_order(data['order'])
        self._delete_dp_order(order)

    def _on_dp_order_executed(self, data):
        order = self._serialize_dp_order(data['order'])
        if order['status'] == 'pending':
            self._update_dp_order(order)
        else:
            self._delete_dp_order(order)

        asyncio.ensure_future(self.bot.on_dp_order_hit(data['other_party_name'], data['hit_amount'],
                                                       float(data['price']), order))

    def _delete_dp_order(self, order):
        swift_id = order['swift_id']
        direction = order['direction']
        order_id = order['id']

        for i, order in enumerate(self.dp_orders[swift_id][direction]):
            if order['id'] == order_id:
                self.dp_orders[swift_id][direction].pop(i)
                return

    def _update_dp_order(self, order):
        swift_id = order['swift_id']
        direction = order['direction']
        order_id = order['id']

        for i, candidate_order in enumerate(self.dp_orders[swift_id][direction]):
            if candidate_order['id'] == order_id:
                self.dp_orders[swift_id][direction][i] = order
                return

    def _on_eb_order_added(self, data):
        order = self._serialize_eb_order(data['order'])
        swift_id = order['swift_id']
        direction = order['direction']

        self.eb_orders[swift_id][direction].append(order)

    def _on_eb_order_cancelled(self, data):
        order = self._serialize_eb_order(data['order'])
        self._delete_eb_order(order)

    def _on_eb_order_executed(self, data):
        market_maker = data['order']['trader_name']
        # We only care about orders placed by the user
        if market_maker != self.trader:
            return

        order = self._serialize_eb_order(data['order'])

        if order['status'] == 'pending':
            self._update_eb_order(order)
        else:
            self._delete_eb_order(order)

        asyncio.ensure_future(
            self.bot.on_eb_order_hit(data['market_taker_name'], data['hit_amount'], float(data['price']), order))

    def _delete_eb_order(self, order):
        swift_id = order['swift_id']
        direction = order['direction']
        order_id = order['id']

        for i, order in enumerate(self.eb_orders[swift_id][direction]):
            if order['id'] == order_id:
                self.eb_orders[swift_id][direction].pop(i)
                return

    def _update_eb_order(self, order):
        swift_id = order['swift_id']
        direction = order['direction']
        order_id = order['id']

        for i, candidate_order in enumerate(self.eb_orders[swift_id][direction]):
            if candidate_order['id'] == order_id:
                self.eb_orders[swift_id][direction][i] = order
                return

    def _on_eb_depth_changed(self, data):
        swift_id = data['swift_id']
        direction = data['direction']
        eb_depth = self._serialize_eb_depth(data['eb_depth'])

        old_eb_depth = self.eb_depth[swift_id][direction]
        self.eb_depth[swift_id][direction] = eb_depth

        asyncio.ensure_future(self.bot.on_eb_depth_change(swift_id, direction, old_eb_depth, eb_depth))

    def _on_macro_event_published(self, data):
        event = self._serialize_macro_event(data['event'])
        asyncio.ensure_future(self.bot.on_macro_event(event))

    def _on_market_price_changed(self, data):
        market_price = self._serialize_market_price(data['market_price'])
        swift_id = market_price['swift_id']

        old_market_price = self.market_prices[swift_id]
        self.market_prices[swift_id] = market_price

        asyncio.ensure_future(self.bot.on_market_price_change(old_market_price, market_price))

    def _on_market_price_chart_updated(self, data):
        new_price = data['point']
        swift_id = new_price['swift_id']

        self.price_curves[swift_id].append(new_price)

    def _on_position_updated(self, data):
        position = self._serialize_position(data['position'])
        swift_id = position['swift_id']

        old_position = self.positions[swift_id]
        self.positions[swift_id] = position

        asyncio.ensure_future(self.bot.on_position_change(old_position, position))

    def _on_pnl_updated(self, data):
        position = self._serialize_position(data['position'])
        swift_id = position['swift_id']

        old_position = self.positions[swift_id]
        # Only update the pnl related fields from the position
        for k in position:
            if k not in ['pnl', 'converted_pnl', 'pnl_percentage', 'return_on_volume']:
                position[k] = old_position[k]
        self.positions[swift_id] = position

        asyncio.ensure_future(self.bot.on_position_change(old_position, position))

    def _serialize_client_call(self, call):
        return {
            'id': call['id'],
            'client': call['client_name'],
            'swift_id': call['swift_id'],
            'direction': call['direction'],
            'amount': call['amount'],
            'client_max_spread': call['client_max_spread'],
            'client_price': None if call['client_price'] == '' else float(call['client_price']),
            'trader_quote': None if call['trader_quote'] == '' else float(call['trader_quote'])
        }

    def _serialize_ib_call(self, call):
        return {
            'id': call['id'],
            'market_taker': call['market_taker_name'],
            'market_maker': call['market_maker_name'],
            'swift_id': call['swift_id'],
            'amount': call['amount'],
            'direction': {IB_CALL_HIT_BID: BID, IB_CALL_HIT_ASK: ASK}.get(call['status'], None),
            'bid': None if call['bid'] == '' else float(call['bid']),
            'ask': None if call['bid'] == '' else float(call['ask'])
        }

    def _serialize_dp_order(self, order):
        return {
            'id': order['id'],
            'room_time': order['room_time'],
            'swift_id': order['swift_id'],
            'order_type': DP_ORDER_TYPE[order['order_type']],
            'direction': order['direction'],
            'amount': order['amount'],
            'price': float(order['price']) if order['price'] is not None else None,
            'status': ORDER_STATUS[order['status']]
        }

    def _serialize_eb_order(self, order):
        return {
            'id': order['id'],
            'room_time': order['room_time'],
            'swift_id': order['swift_id'],
            'order_type': EB_ORDER_TYPE[order['order_type']],
            'direction': order['direction'],
            'amount': order['amount'],
            'chunk': order['chunk'],
            'visible_amount': order['visible_amount'],
            'price': float(order['price']),
            'status': ORDER_STATUS[order['status']]
        }

    def _serialize_eb_depth(self, eb_depth):
        def serialize_depth_order(order):
            return {
                'count': order['count'],
                'amount': order['amount'],
                'price': float(order['price'])
            }

        return [serialize_depth_order(eb_order) for eb_order in eb_depth]

    def _serialize_macro_event(self, event):
        return {
            'event_type': {0: 'news', 1: 'forecast'}[event['event_type']],
            'event_data': event['event_data']
        }

    def _serialize_market_price(self, market_price):
        return {
            'swift_id': market_price['swift_id'],
            'open': float(market_price['open']),
            'bid': float(market_price['bid']),
            'ask': float(market_price['ask']),
            'price': float(market_price['price'])
        }

    def _serialize_price_curve_price(self, price):
        return {
            'swift_id': price['swift_id'],
            'room_time': price['room_time'],
            'price': float(price['price'])
        }

    def _serialize_position(self, position):
        return {
            'swift_id': position['swift_id'],
            'amount': position['amount'],
            'rate': float(position['rate']),
            'pnl': position['pnl'],
            'converted_pnl': position['converted_pnl'],
            'pnl_percentage': position['pnl_percentage'],
            'return_on_volume': position['return_on_volume'],
            'max_position': position['max_position'],
            'total_volume': position['total_volume'],
            'yield': None if position.get('yield') is None else float(position['yield']),
            'dv01': position.get('dv01'),
            'day_change': position.get('day_change')
        }
