# -*- coding: utf-8 -*-
"""
Created on Tue Sep 28 08:37:00 2022

@author: yqlim
"""
import logging
logger = logging.getLogger(__name__)

from typing import Union, Any, Callable, Dict, List
import itertools
import datetime
from dateutil import parser
from dateutil.relativedelta import relativedelta
import calendar
import pytz
import hmac
import hashlib
from uuid import uuid4
import asyncio
import numpy as np
import copy
import websockets
import websocket
import json

from .base import AsyncWebsocket, SyncWebsocket
from ..finance.base import Order
from ..finance.market_data.rates import ForwardCurve
from ..finance.derivatives.vanilla.instruments import OptionCrypto, ENUM_OPTION_TYPE, ENUM_OPTION_STYLE
from ..finance.spot.instruments import Cryptocurrency
from ..utils.typing_ import to_list

class DeribitContractsFutures():
    def __init__(self, as_at: int) -> None:
        """
        Wrapper that contains information on
        Deribit futures contracts. 
        
        Parameters
        ----------
        as_at : int:
            date in yyyymmdd format, rollover
            at 0800 UTC - 20240724 refers to 
            the period after 20240723 08:00 UTC
            and before 20240724 08:00 UTC
        """
        
        # defined in self.as_at setter
        self.as_at_datetime = None
        self.first_day = None
        self.last_day = None
        self.last_date = None
        self.last_friday = None
        self.contract_month = None
        self.contract_year = None
        
        self.as_at = as_at
        
    
    @property
    def as_at(self) -> int:
        return self.__int_as_at
    
    @as_at.setter
    def as_at(self, as_at: int) -> None:
        self.__int_as_at = as_at
        self.as_at_datetime = datetime.datetime.strptime(str(self.as_at), '%Y%m%d')
        
        self.first_day, self.last_day = calendar.monthrange(self.as_at_datetime.year, self.as_at_datetime.month)
        self.last_date = self.as_at_datetime.replace(day = self.last_day)
        self.last_friday = self.last_date + datetime.timedelta(days=-(-(-self.last_date.weekday()-3)%7))
        
        contract_month = (
            self.as_at_datetime + 
            relativedelta(
                months = (self.as_at_datetime > self.last_friday) * 1
            )
        )
        self.contract_month = contract_month.month
        self.contract_year  = contract_month.year
    
    def quarterlies(self) -> set:
        """
        Generates list of expiries for 
        quarterly contracts.
        """
        next_quarter = self.as_at_datetime.replace(
            day=1, 
            month=self.contract_month+(3-self.contract_month%3),
            year=self.contract_year
        )
        quarters = [next_quarter+relativedelta(months=i*3) for i in range(4)]
        quarterly_expiries = []
        for quarter in quarters:
            first_day, last_day = calendar.monthrange(quarter.year, quarter.month)
            last_date = quarter.replace(day=last_day)
            quarterly_expiries.append(last_date + datetime.timedelta(days=-(-(-last_date.weekday()-3)%7)))
        quarterly_expiries = [q.replace(hour=8, tzinfo=pytz.utc) for q in quarterly_expiries]
        return set(quarterly_expiries)
    
    def monthlies(self) -> set:
        """
        Generates list of expiries for 
        monthly contracts.
        """
        monthly_expiries = []
        monthlies = [self.as_at_datetime.replace(day=1, month=self.contract_month, year=self.contract_year) + relativedelta(months=i) for i in range(2)]
        for month in monthlies:
            first_day, last_day = calendar.monthrange(month.year, month.month)
            last_date = month.replace(day=last_day)
            monthly_expiries.append(last_date + datetime.timedelta(days=-(-(-last_date.weekday()-3)%7)))     
        monthly_expiries = [m.replace(hour=8, tzinfo=pytz.utc) for m in monthly_expiries]
        return set(monthly_expiries)
    
    def weeklies(self) -> set:
        """
        Generates list of expiries for 
        weekly contracts.
        """
        last_friday = self.as_at_datetime + datetime.timedelta(days=-(-(-self.as_at_datetime.weekday()-3)%7))
        weekly_expiries = [
            (last_friday + datetime.timedelta(days=7)).replace(hour=8, tzinfo=pytz.utc),
            (last_friday + datetime.timedelta(days=14)).replace(hour=8, tzinfo=pytz.utc)
        ]
        return set(weekly_expiries)
        
    def available_expiries(self) -> set:
        """
        Generates set of all available
        expiries.
        """
        expiries = [self.quarterlies(), self.monthlies(), self.weeklies()]
        return set.union(*expiries)

class OrderBookDeribit():
    def __init__(self, bids: list[list[float]], asks: list[list[float]]) -> None:
        """
        Create an instance of an orderbook.
        
        Parameters
        ----------
        bids : list[list[float]]
            in the format [ [bid_1, volume_1], [bid_2, volume_2], 
            ..., [bid_n, volume_n] ], where bid_1 > ... > bid_n
        asks : list[list[float]]
            in the format [ [ask_1, volume_1], [ask_2, volume_2], 
            ..., [ask_n, volume_n] ], where ask_1 < ... < ask_n
        """
        self.bids = sorted(bids, key=lambda x: x[0], reverse=True)
        self.asks = sorted(asks, key=lambda x: x[0])
        
    @property
    def top_of_book(self) -> tuple[float]:
        """
        Top bid and ask price
        
        Returns
        -------
        tuple[float]
        """
        return self.bids[0][0] if self.bids else None, self.asks[0][0] if self.asks else None
    
    @property
    def mid(self) -> float:
        """
        Average of top bid and ask
        """
        bid, ask = self.top_of_book
        if bid and ask:
            return (bid + ask) / 2.0
        else:
            if bid:
                return bid
            if ask:
                return ask
        return None
    
    @property
    def spread(self) -> float:
        """
        Absolute spread given by top ask minus
        top bid
        
        Returns
        -------
        float
        """
        bid, ask = self.top_of_book
        if bid and ask:
            return ask-bid
        return None
    
    @property
    def spread_percentage(self) -> float:
        """
        Bid-ask spread as a percentage of mid
        
        Returns
        -------
        float
        """
        if self.spread:
            return self.spread / self.mid
        return None
    
    def get_buy_volume_ratio(self, num_of_levels : list[int]) -> float:
        """
        Cumulative bid volume divided by sum of
        cumulative bid and ask volume.
        
        Parameters
        ----------
        num_of_levels : list[int]
            a value of 1 will give the buy volume
            ratio for top of book
        
        Returns
        -------
        list[float]
            buy volume ratio in ascending order 
            of num_of_levels - if num_of_levels
            of [1, 10, 5] is given, buy volume
            ratios will be returned in order of
            [1, 5, 10]
        """
        max_level = max(to_list(num_of_levels))
        max_level_bid = len(self.bids)
        max_level_ask = len(self.asks)
        cum_bid_volume = 0
        cum_ask_volume = 0
        output = []
        if not (max_level_bid==0 and max_level_ask==0):
            for i in range(max_level):
                if i < max_level_bid:
                    cum_bid_volume += self.bids[i][1]
                if i < max_level_ask:
                    cum_ask_volume += self.asks[i][1]

                if (i+1) in num_of_levels:
                    total_volume = cum_bid_volume + cum_ask_volume
                    if total_volume == 0:
                        output.append(0.0)
                    else:
                        output.append(cum_bid_volume / total_volume)
            return output
        else:
            return [0.0] * len(num_of_levels)

        
    def get_cumulative_volume(self, num_of_levels : list[int], side: str) -> float:
        """
        Cumulative dollar volume.
        
        Parameters
        ----------
        num_of_levels : list[int]
            a value of 1 will give the buy volume
            ratio for top of book
            
        side : str
            bid or ask
        
        Returns
        -------
        list[float]
            buy volume ratio in ascending order 
            of num_of_levels - if num_of_levels
            of [1, 10, 5] is given, buy volume
            ratios will be returned in order of
            [1, 5, 10]
        """
        side = side.strip().lower()
        max_level = max(to_list(num_of_levels))
        if side == 'bid':
            max_level_available = len(self.bids)
        else:
            max_level_available = len(self.asks)    
        cum_volume = 0
        output = []
        if not max_level_available==0:
            for i in range(max_level_available):

                if side == 'bid':
                    cum_volume += self.bids[i][1]
                else:
                    cum_volume += self.asks[i][1]

                if (i+1) in num_of_levels:
                    output.append(cum_volume)
            return output + (len(num_of_levels) - len(output)) * [0.0]
        else:
            return [0.0] * len(num_of_levels)    

class DeribitOrder(Order):
    def __init__(self, instrument_name: str, amount: float, type_: str,
                    label: str, price: float, time_in_force: str, max_show: float = 0.0,
                    post_only: bool = True, reject_post_only: bool = False,
                    reduce_only: bool = False, trigger_price: float = None,
                    trigger_offset: float = None, trigger: str = '',
                    advanced: str = '', mmp: bool = False, valid_until: int = 0):
        super().__init__(instrument_name, amount, price)
        self.instrument_name = instrument_name
        self.amount = amount 
        self.type_ = type_
        self.label = label
        self.price = price
        self.time_in_force = time_in_force
        self.max_show = max_show
        self.post_only = post_only
        self.reject_post_only = reject_post_only
        self.reduce_only = reduce_only
        self.trigger_price = trigger_price
        self.trigger_offset = trigger_offset
        self.trigger = trigger
        self.advanced = advanced
        self.mmp = mmp
        self.valid_until = valid_until

    def payload(self) -> Dict:
        payload = {
            'instrument_name' : self.instrument_name,
            'amount' : self.amount,
            'type' : self.type_,
            'label' : self.label,
            'price' : self.price,
            'time_in_force' : self.time_in_force,
            'max_show' : self.max_show,
            'reject_post_only' : self.reject_post_only,
            'reduce_only' : self.reduce_only,
            'trigger_price' : self.trigger_price,
            'trigger_offset' : self.trigger_offset,
            'trigger' : self.trigger,
            'advanced' : self.advanced,
            'mmp' : self.mmp
        }
        if payload['time_in_force'] == 'good_til_cancelled':
            payload['post_only'] = self.post_only
        if self.valid_until:
            payload['valid_until'] = self.valid_until
        return payload
    
class DeribitEMS(SyncWebsocket):
    _END_POINT = 'wss://www.deribit.com/ws/api/v2'
    
    def __init__(self, api_key : Union[None, str] = None, 
                    api_secret : Union[None, str] = None, 
                    client_id : Union[None, str] = None,
                    disable_heartbeat: bool=True) -> None:

        super().__init__(self._END_POINT)

        self.api_key = api_key
        self.api_secret = api_secret
        self.client_id = str(uuid4()) if client_id is None else client_id
        self.disable_heartbeat = disable_heartbeat
        self.refresh_token = ''
        self.msg = {'jsonrpc' : '2.0',
                    'id' : self.client_id,
                    'method' : None}

    def connect(self) -> None:
        self.websocket = websocket.create_connection(self.endpoint, timeout=86400)
        if self.disable_heartbeat:
            self._disable_heartbeat()
        self._subscribe_user_portfolio(['BTC', 'ETH'])

    def __enter__(self):
        self.connect()
        return self.websocket
    
    def __exit__(self, exc_type, exc, tb) -> None:
        if self.websocket:
            self.websocket.close()

    def _to_list(self, param: Union[List, str]) -> List[str]:
        if not isinstance(param, list):
            return [param]
        else:
            return param

    def _get_signature(self) -> Dict:
        """
        Generate Deribit signature.
        """
        msg = self.msg.copy()
        msg['method'] = 'public/auth'
        params = {
            'grant_type' : 'client_credentials',
            'client_id' : self.api_key,
            'client_secret' : self.api_secret
        }
        msg['params'] = params
        return msg

    def _disable_heartbeat(self) -> Dict:
        auth = self._get_signature()
        msg = self.msg.copy()                
        msg['method'] = 'public/disable_heartbeat'        
        if self.websocket:
            self.websocket.send(json.dumps(auth))
            response = self.websocket.recv()  
            self.websocket.send(json.dumps(msg))
            response = self.websocket.recv()  
            

    def _subscribe_user_portfolio(self, currencies: List[str], 
                                    func: Callable=print) -> None:
        auth = self._get_signature()
        currencies = self._to_list(currencies)
        channels = [
            'user.portfolio.{currency}'.format(currency=currency)
            for currency in currencies
        ]
        msg = self.msg.copy()
        msg['method'] = 'private/subscribe'
        params = {'channels' : channels}
        msg['params'] = params
        if self.websocket:
            self.websocket.send(json.dumps(msg))

    def process_action(self, action) -> None:
        task_name = action.pop('task_name', None)
        try:
            if task_name == 'ORDER':               
                response = self.send_order(action)
            elif task_name == 'CANCEL':
                response = self.cancel_order(action['order_id'])
            elif task_name == 'REFRESH':
                response = self._refresh_token({})            
        except Exception as e:
            print(e)

    def _refresh_token(self, payload: Dict) -> Dict:
        print('***refresh***')
        auth = self._get_signature()
        auth['params']['scope'] = 'trade:read_write' 
        auth['params']['refresh_token'] = self.refresh_token 
        self.websocket.send(json.dumps(auth))  
        response = self.websocket.recv()   
        return json.loads(response)     

    def send_order(self, payload: Dict) -> Dict:
        auth = self._get_signature()
        msg = self.msg.copy()
        amount = float(payload['amount'])
        if amount > 0.0:
            msg['method'] = 'private/buy'
        else:
            msg['method'] = 'private/sell'
            payload['amount'] = abs(payload['amount'])
        params = payload  
        auth['params']['scope'] = 'trade:read_write' 
        msg['params'] = params          
        
        self.websocket.send(json.dumps(auth))        
        response = self.websocket.recv()
        self.websocket.send(json.dumps(msg))        
        response = json.loads(self.websocket.recv())   
        
        if 'refresh_token' in response['result']:   
            self.refresh_token = response['result']['refresh_token'] 
        return response

    def cancel_order(self, order_id: str) -> Dict:        
        auth = self._get_signature()
        msg = self.msg.copy()      
        msg['method'] = 'private/cancel'
        params = {'order_id' : order_id}  
        auth['params']['scope'] = 'trade:read_write' 
        msg['params'] = params  

        self.websocket.send(json.dumps(auth))
        response = self.websocket.recv()        
        self.websocket.send(json.dumps(msg))
        response = self.websocket.recv()        

        return json.loads(response)            

class DeribitWebsocket(AsyncWebsocket):
    _END_POINT = 'wss://www.deribit.com/ws/api/v2'
    _REQUEST_PER_SECOND = 15
    _SUPPORTED_CURRENCIES = ['BTC', 'ETH']

    def __init__(self, api_key : Union[None, str] = None, 
                    api_secret : Union[None, str] = None, 
                    client_id : Union[None, str] = None) -> None:

        super().__init__(self._END_POINT, self._REQUEST_PER_SECOND)

        self.api_key = api_key
        self.api_secret = api_secret
        self.client_id = str(uuid4()) if client_id is None else client_id
        self.msg = {'jsonrpc' : '2.0',
                    'id' : self.client_id,
                    'method' : None}

    def _to_list(self, param: Union[List, str]) -> List[str]:
        if not isinstance(param, list):
            return [param]
        else:
            return param

    def _get_signature(self) -> Dict:
        """
        Generate Deribit signature.
        """
        msg = self.msg.copy()
        msg['method'] = 'public/auth'
        params = {
            'grant_type' : 'client_credentials',
            'client_id' : self.api_key,
            'client_secret' : self.api_secret
        }
        msg['params'] = params
        return msg
    
    def get_book_summary_by_currency(self, currencies: List[str], kind: str) -> Dict:
        messages = []
        currencies = self._to_list(currencies)
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_book_summary_by_currency'
            params = {
                'currency' : currency,
                'kind' : kind
            }
            msg['params'] = params        
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_book_summary_by_instrument(self, instrument_names: List[str]) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_book_summary_by_instrument'
            params = {
                'instrument_name' : instrument_name
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_contract_size(self, instrument_names: List[str]) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_contract_size'
            params = {
                'instrument_name' : instrument_name
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_currencies(self) -> Dict:
        msg = self.msg.copy()
        msg['method'] = 'public/get_currencies'
        params = {}
        msg['params'] = params
        return self.loop(self._get, msg)

    def get_delivery_prices(self, index_names: List[str], offset: int=0, count: int=10) -> Dict:
        index_names = self._to_list(index_names)
        supported_indices = ['ada_usd', 'avax_usd', 'btc_usd', 'eth_usd', 'dot_usd', 
                                'matic_usd', 'sol_usd', 'usdc_usd', 'xrp_usd', 'ada_usdc',
                                'avax_usdc', 'btc_usdc', 'eth_usdc', 'dot_usdc', 'matic_usdc',
                                'sol_usdc', 'xrp_usdc', 'btcdvol_usdc', 'ethdvol_usdc']
        supported_index_names = [index_name.strip().lower() 
                        for index_name in index_names 
                        if index_name in supported_indices]        
        if len(supported_index_names) == len(index_names):
            messages = []
            for index_name in supported_index_names:
                msg = self.msg.copy()
                msg['method'] = 'public/get_delivery_prices'
                params = {
                    'index_name' : index_name,
                    'offset' : offset,
                    'count' : count
                }            
                msg['params'] = params
                messages.append(msg)
            return self.gather(self._get, messages)
        else:
            diff = set(index_names) - set(supported_index_names)
            raise ValueError('unsupported index name(s) - {diff}'.format(diff=diff))

    def get_funding_chart_data(self, instrument_names: List[str], length: str) -> Dict:
        instrument_names = self._to_list(instrument_names)
        supported_length = ['8h', '24h', '1m']
        if length in supported_length:
            messages = []
            for instrument_name in instrument_names:
                msg = self.msg.copy()
                msg['method'] = 'public/get_funding_chart_data'
                params = {
                    'instrument_name' : instrument_name,
                    'length' : length
                }
                msg['params'] = params
                messages.append(msg)
            return self.gather(self._get, messages)
        else:
            raise ValueError('unsupported length - {length}'.format(length=length))
    
    def get_funding_rate_history(self, instrument_names: List[str], start_timestamp: int,
                                    end_timestamp: int) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_funding_rate_history'
            params = {
                'instrument_name' : instrument_name,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_funding_rate_value(self, instrument_names: List[str], start_timestamp: int, 
                                    end_timestamp: int) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_funding_rate_value'
            params = {
                'instrument_name' : instrument_name,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_historical_volatility(self, currencies: List[str]) -> Dict:
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_historical_volatility'
            params = {
                'currency' : currency
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_index_price(self, index_names: List[str]) -> Dict:
        index_names = self._to_list(index_names)
        messages = []
        for index_name in index_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_index_price'
            params = {
                'index_name' : index_name
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_index_price_names(self) -> Dict:
        msg = self.msg.copy()
        msg['method'] = 'public/get_index_price_names'
        params = {
        }
        msg['params'] = params
        return self.loop(self._get, msg)

    def get_instrument(self, instrument_names: List[str]):
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_instrument'
            params = {
                'instrument_name' : instrument_name
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)
    
    def get_instruments(self, currencies: List[str], kind: str='', 
                            expired: bool=False) -> Dict:
        """
        Get instruments listed on Deribit.

        Parameters
        ----------
        currency : str
            underlying of instrument - must be one of `DeribitWebsocket._SUPPORTED_CURRENCIES`
        kind : str
            type of instrument - must be one of `DeribitWebsocket._SUPPORT_INSTRUMENT`
        expired : bool
            filter condition for instruments to be returned by API

        Return
        ------
        list of dict
            each dictionary is a json representation of a future object
        """
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_instruments'
            params = {
                'currency' : currency,                
                'expired' : expired
            }
            if kind:
                params['kind'] = kind
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_last_settlements_by_currency(self, currencies: List[str], type_: str,
                                            count: int, continuation: str,
                                            search_start_timestamp: int) -> Dict:
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_last_settlements_by_currency'
            params = {
                'currency' : currency,
                'type' : type_,
                'count' : count,
                'continuation' : continuation,
                'search_start_timestamp' : search_start_timestamp
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_last_settlements_by_instrument(self, instrument_names: List[str], type_: str,
                                            count: int, continuation: str, 
                                            search_start_timestamp: int) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_last_settlements_by_instrument'
            params = {
                'instrument_name' : instrument_name,
                'type' : type_,
                'count' : count,
                'continuation' : continuation,
                'search_start_timestamp' : search_start_timestamp
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_last_trades_by_currency(self, currencies: List[str], kind: str,
                                        start_id: str, end_id: str, count: int,
                                        include_old: bool, sorting: str) -> Dict:
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_last_trades_by_currency'
            params = {
                'currency' : currency,
                'kind' : kind,
                'count' : count,
                'include_old' : include_old,
                'sorting' : sorting
            }
            if start_id:
                params['start_id'] = start_id
            if end_id:
                params['end_id'] = end_id

            msg['params'] = params
            messages.append(msg)
        
        return self.gather(self._get, messages)

    def get_last_trades_by_currency_and_time(self, currencies: List[str], 
                                        start_timestamp: int, end_timestamp: int, 
                                        kind: str = 'any', count: int = 10, 
                                        include_old: bool = False, 
                                        sorting: str = 'default') -> Dict:
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_last_trades_by_currency_and_time'
            params = {
                'currency' : currency,
                'kind' : kind,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp,
                'count' : count,
                'include_old' : include_old,
                'sorting' : sorting
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_last_trades_by_instrument(self, instrument_names: List[str], 
                                        start_seq: int = -99, end_seq: int = -99,
                                        count: int = 10, include_old: bool = False,
                                        sorting: str = 'default') -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_last_trades_by_instrument'
            params = {
                'instrument_name' : instrument_name,
                'count' : count,
                'include_old' : include_old,
                'sorting' : sorting
            }
            if start_seq != -99:
                params['start_seq'] = start_seq
            if end_seq != -99:
                params['end_seq'] = end_seq
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)
    
    def get_last_trades_by_instrument_and_time(self, instrument_names: List[str],
                                        start_timestamp: int, end_timestamp: int,
                                        count: int = 10, include_old: bool = False,
                                        sorting: str = 'default') -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_last_trades_by_instrument_and_time'
            params = {
                'instrument_name' : instrument_name,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp,
                'count' : count,
                'include_old' : include_old,
                'sorting' : sorting
            }
            msg['params'] = params
        messages.append(msg)
        return self.gather(self._get, messages)

    def get_mark_price_history(self, instrument_names: List[str],
                                start_timestamp: int, end_timestamp: int) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_mark_price_history'
            params = {
                'instrument_name' : instrument_name,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_order_book(self, instrument_names: List[str], depth: int = -99) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_order_book'
            params = {
                'instrument_name' : instrument_name
            }
            if depth != -99:
                params['depth'] = depth
            msg['params'] = params
            messages.append(msg)        
        #return asyncio.run(self.gather(self._get, messages))
        order_books = self.gather(self._get, messages)
        return order_books

    def get_order_book_by_instrument_id(self, instrument_ids: List[int],
                                            depth: int = -99) -> Dict:
        instrument_ids = self._to_list(instrument_ids)
        messages = []
        for instrument_id in instrument_ids:
            msg = self.msg.copy()
            msg['method'] = 'public/get_order_book_by_instrument_id'
            params = {
                'instrument_id' : instrument_id
            }
            if depth != -99:
                msg['depth'] = depth
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_rfqs(self, currencies: List[str], kind: str = '') -> Dict:
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_rfqs'
            params = {
                'currency' : currency
            }
            if kind:
                params['kind'] = kind
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)
    
    def get_trade_volumes(self, extended: bool = False) -> Dict:
        msg = self.msg.copy()
        msg['method'] = 'public/get_trade_volumes'
        params = {
            'extended' : extended
        }
        msg['params'] = params
        return self.loop(self._get, msg)
    
    def get_tradingview_chart_data(self, instrument_names: List[str], 
                                start_timestamp: int, end_timestamp: int,
                                resolution: str) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/get_tradingview_chart_data'
            params = {
                'instrument_name' : instrument_name,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp,
                'resolution' : resolution
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_volatility_index_data(self, currencies: List[str], start_timestamp: int,
                                    end_timestamp: int, resolution: str) -> Dict:
        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'public/get_volatility_index_data'
            params = {
                'currency' : currency,
                'start_timestamp' : start_timestamp,
                'end_timestamp' : end_timestamp,
                'resolution' : resolution
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)

    def get_ticker(self, instrument_names: List[str]) -> Dict:
        instrument_names = self._to_list(instrument_names)
        messages = []
        for instrument_name in instrument_names:
            msg = self.msg.copy()
            msg['method'] = 'public/ticker'
            params = {
                'instrument_name' : instrument_name
            }
            msg['params'] = params
            messages.append(msg)
        return self.gather(self._get, messages)        

    def get_account_summaries(self, currencies: List[str], 
            subaccount_id: int = -99, extended: bool=False):   
        """
        Get account summary from Deribit.

        Parameters
        ----------
        currencies : list of str
            underlying of instrument - must be one of `DeribitWebsocket._SUPPORTED_CURRENCIES`
        subaccount_id : int
        extended : bool
            includes additional fields (default is `False`)

        Return
        ------
        list of dict
            each dictionary is a json representation of a future object
        """        
        auth = self._get_signature()
        auth['params']['scope'] = 'account:read'

        currencies = self._to_list(currencies)
        messages = []
        for currency in currencies:
            msg = self.msg.copy()
            msg['method'] = 'private/get_account_summary'        
            params = {'currency': currency,
                        'extended' : extended} 
            if subaccount_id != -99:
                params['subaccount_id'] = subaccount_id
            msg['params'] = params  
            messages.append(msg)
        return self.gather(self._get, messages, {}, {}, auth)

    def get_option_chain(self, currency: str) -> List[Dict]:
        instruments = self.get_instruments(currencies = [currency], 
                                            kind = 'option', 
                                            expired = False)
        instrument_names = []
        for currency in instruments:
            for instrument in currency['result']:
                instrument_names.append(instrument['instrument_name'])        
        
        order_books = self.get_order_book(instrument_names, 3)        

        instruments = []
        for response in order_books:
            instrument = DeribitInstrument()  
            instrument.from_api(response['result'])          
            instruments.append(instrument)

        return instruments
        

    def get_futures(self, currency: str, expired: bool = False):
        """
        Helper function that returns futures listed on Deribit by currency.

        Parameters
        ----------
        currency : str
            coin to which to retrieve futures
        expired : bool, optional
            to include expired instruments (default is `False`, which implies that
            expired instruments will not be included)
        """
        if currency in self._SUPPORTED_CURRENCIES:
            instruments = self.get_instruments([currency], 'future', expired)
            print(instruments)
            instruments = instruments[0]['result']

            output = []
            for instrument in instruments:
                output.append(DeribitInstrument(instrument))

            return output
        else:
            raise ValueError('unsupported `currency`.')


    def subscribe_order_book(self, instrument_names: List[str], group: str='100',
                                depth: int=1, interval: str='100ms', 
                                func: Callable=print, auth: Dict={}) -> None:
        instrument_names = self._to_list(instrument_names)
        channels = []
        for instrument_name in instrument_names:
            channel = '.'.join(['book', instrument_name, str(group), str(depth), interval])        
            channels.append(channel)

        msg = self.msg.copy()
        msg['method'] = 'public/subscribe'        
        params = {'channels' : channels}        
        msg['params'] = params        
        self.subscribe(self._subscribe, msg, func, auth)

    def subscribe_trades_by_currency(self, currencies: List[str], kinds: List[str],
                                        interval: str, func: Callable=print,
                                        auth: Dict={}) -> None:
        auth = self._get_signature()
        currencies = self._to_list(currencies)
        channels = []
        for currency in currencies:
            for kind in kinds:
                channel = '.'.join(['trades', kind, currency, interval])
                channels.append(channel)
        
        msg = self.msg.copy()
        msg['method'] = 'private/subscribe'
        params = {
            'channels' : channels
        }
        msg['params'] = params
        self.subscribe(self._subscribe, msg, func, auth)

    def subscribe_user_changes_by_instruments(self, instrument_names:List[str], interval: str='raw',
                                    func: Callable=print) -> None:
        auth = self._get_signature()
        instrument_names = self._to_list(instrument_names)
        channels = ['user.changes.{instrument_name}.{interval}'.format(
            instrument_name=instrument_name, interval=interval)
            for instrument_name in instrument_names
        ]

        msg = self.msg.copy()
        msg['method'] = 'private/subscribe'        
        params = {'channels' : channels}        
        msg['params'] = params
        
        self.subscribe(self._subscribe, msg, func, auth)

    def subscribe_user_changes_by_currency(self, currencies:List[str], 
        kind: str, interval: str='raw', func: Callable=print) -> None:
        auth = self._get_signature()
        currencies = self._to_list(currencies)
        channels = ['user.changes.{kind}.{currency}.{interval}'.format(
            currency=currency, kind=kind, interval=interval)
            for currency in currencies
        ]

        msg = self.msg.copy()
        msg['method'] = 'private/subscribe'        
        params = {'channels' : channels}        
        msg['params'] = params
        
        self.subscribe(self._subscribe, msg, func, auth)

    def subscribe_user_portfolio(self, currencies: List[str], 
                                    func: Callable=print) -> None:
        auth = self._get_signature()
        currencies = self._to_list(currencies)
        channels = [
            'user.portfolio.{currency}'.format(currency=currency)
            for currency in currencies
        ]
        
        msg = self.msg.copy()
        msg['method'] = 'private/subscribe'
        params = {'channels' : channels}
        msg['params'] = params

        self.subscribe(self._subscribe, msg, func, auth)

    def subscribe_changes_by_currency(
        self, currencies: List[str], kinds: List[str], 
        interval: str='raw', func: Callable=print
    ) -> None:
        
        auth = self._get_signature()
        currencies = self._to_list(currencies)
        channels = []
        for i in range(len(currencies)):
            for j in range(len(kinds)):
                channels.append(
                    'user.changes.{kind}.{currency}.{interval}'.format(
                        currency=currencies[i], 
                        kind=kinds[j].lower(), 
                        interval=interval
                    )
                )
                channels.append(
                    'trades.{kind}.{currency}.{interval}'.format(
                        currency=currencies[i], 
                        kind=kinds[j].lower(), 
                        interval=interval
                    )
                )                

        channels.extend(
            [
                'user.portfolio.{currency}'.format(currency=currency)
                for currency in currencies
            ]                
        )
        msg = self.msg.copy()
        msg['method'] = 'private/subscribe'        
        params = {'channels' : channels}        
        msg['params'] = params
        
        self.subscribe(self._subscribe, msg, func, auth)


    # def buy(self, instrument_name: str, amount: float, order_type: str,
    #             reduce_only: bool, price: Union[None, float], 
    #             post_only: Union[None, bool] = None) -> Dict:
    #     auth = self._get_signature()
    #     msg = self.msg.copy()
    #     msg['method'] = 'private/buy'
    #     params = {
    #         'instrument_name' : instrument_name,
    #         'amount' : amount,
    #         'type' : order_type,
    #         'reduce_only' : reduce_only
    #     }
    #     if price:
    #         params['price'] = price
    #     if post_only:
    #         params['post_only'] = post_only        
    #     msg['params'] = params
    #     self.loop(self._get, msg, auth)

    def buy(self, order: DeribitOrder) -> Dict:
        auth = self._get_signature()
        msg = self.msg.copy()
        msg['method'] = 'private/buy'
        params = order.payload()  
        auth['params']['scope'] = 'trade:read_write'        
        # params['grant_type'] = 'client_credentials'
        # auth['scope'] = 'trade:read_write'  
        msg['params'] = params        

        return self.loop(self._get, msg, {}, {}, auth)    
    
    def simulate_portfolio(self, 
            currency: str, 
            simulated_positions: dict, 
            add_positions: bool = False
        ) -> Dict:
        auth = self._get_signature()
        msg = self.msg.copy()
        msg['method'] = 'private/simulate_portfolio'
        params = {
            'currency' : currency,
            'add_positions' : add_positions,
            'simulated_positions' : simulated_positions
        }  

        msg['params'] = params
        return self.loop(self._get, msg, {}, {}, auth)     

class DeribitInstrument(object):
    def __init__(self, attributes: Union[None, Dict[str, Any]] = None) -> None:
        if not attributes is None:
            self.from_api(attributes)

    def from_api(self, attributes: Dict[str, Any]) -> None:
        """
        Instantiates object with attributes from Deribit API output.

        Parameters
        ----------
        attributes : dict
            key-value pairs returned from Deribit endpoint
        """
        if isinstance(attributes, dict):
            for k, v in attributes.items():
                setattr(self, k, v)
        else:
            raise TypeError('`attributes` must be of `dict` type.')

def build_forward_curve(underlying: List[str]) -> ForwardCurve:
    """
    Build forward curves from Deribit for given underlying.

    Parameters
    ----------
    underlying : list of str
        underlying with fixed maturity futures listed on Deribit
    
    Return
    ------
    dict
        dictionary containing mid, bid and ask `ForwardCurve` objects in 
        the format of `{underlying: {'mid':ForwardCurve, 'bid':ForwardCurve, 'ask':ForwardCurve}}`
    """
    # check inputs
    if isinstance(underlying, str):
        underlying = [underlying]    
    underlying = [und.strip().upper() for und in underlying]
    instrument_names = {und.strip().upper():[] for und in underlying}
    
    # get fixed maturity futures instrument names    
    client = DeribitWebsocket()
    summary = client.get_book_summary_by_currency(underlying, 'future')

    # get benchmark indices of fixed maturity 
    indices = []
    for i in range(len(underlying)):
        index = []
        for instrument in summary[i]['result']:
            index.append('_'.join([
                                instrument['base_currency'].strip().lower(),
                                instrument['quote_currency'].strip().lower()]
                                )
            )
            if not 'PERPETUAL' in instrument['instrument_name']:
                instrument_names[instrument['base_currency']].append(instrument)
        index = list(set(index))
        indices += index
        
    index_prices = client.get_index_price(indices)
    index_prices = [float(idx['result']['index_price']) for idx in index_prices] 
    
    curves = {und.strip().upper():{'bid':None, 'ask':None, 'mid':None} for und in underlying}
    for i in range(len(underlying)):
        dates = []
        mid_prices = []
        bid_prices = []
        ask_prices = []
        und = underlying[i].strip().upper()
        for data in instrument_names[und]:
            instrument_name = data['instrument_name']
            uid = '.'.join([und, 'FORWARD'])
            expiry = parser.parse(instrument_name.split('-')[1]).replace(hour=8, minute=0, second=0, tzinfo=pytz.utc) 
            dates.append(expiry)

            mid_prices.append(data['mid_price'])
            bid_prices.append(data['bid_price'])
            ask_prices.append(data['ask_price'])
        
        forward_curve = ForwardCurve(dates=np.array(dates), prices=np.array(mid_prices), curve_id=uid+'.MID')
        forward_curve.calibrate(index_prices[i])        
        curves[und]['mid'] = forward_curve

        forward_curve = ForwardCurve(dates=np.array(dates), prices=np.array(bid_prices), curve_id=uid+'.BID')
        forward_curve.calibrate(index_prices[i])        
        curves[und]['bid'] = forward_curve

        forward_curve = ForwardCurve(dates=np.array(dates), prices=np.array(ask_prices), curve_id=uid+'.ASK')
        forward_curve.calibrate(index_prices[i])        
        curves[und]['ask'] = forward_curve

    return curves

def create_option(instrument_name: str) -> OptionCrypto:
    """
    Function to create an instance of `OptionCrypto` using 
    instrument name from Deribit.

    Parameters
    ----------
    instrument_name : str
        instrument name from Deribit API
    
    Return
    ------
    OptionCrypto
        an instance of `OptionCrypto`
    """
    underlying, expiration, strike, option_type = instrument_name.split('-')  
    underlying = Cryptocurrency(underlying)
    expiration = parser.parse(expiration)
    expiration = expiration.replace(hour=8, minute=0, second=0, tzinfo=pytz.utc)
    option_type = option_type.strip().upper()
    if option_type == 'P':
        option_type = ENUM_OPTION_TYPE.PUT
    elif option_type == 'C':  
        option_type = ENUM_OPTION_TYPE.CALL
    else:
        raise NotImplementedError('`option_type` not implemented.')  
    return OptionCrypto(expiration=expiration, 
                            strike=float(strike), 
                            option_type=option_type, 
                            option_style=ENUM_OPTION_STYLE.EUROPEAN,
                            underlying = underlying)

def build_option_chain(underlying: List[str]) -> List[OptionCrypto]:
    client = DeribitWebsocket()
    instruments = client.get_instruments(currencies = underlying, 
                                        kind = 'option', 
                                        expired = False)
    instrument_names = []
    for currency in instruments:
        for instrument in currency['result']:
            instrument_names.append(instrument['instrument_name'])        
    
    order_books = client.get_order_book(instrument_names, 3)        

    instruments = []
    for response in order_books:
        instrument = response['result']
        option = create_option(instrument['instrument_name'])        
        bid_price = float(instrument['bids'][0][0]) if instrument['bids'] else 0.0        
        ask_price = float(instrument['asks'][0][0]) if instrument['asks'] else 0.0
        option.price = {
            'bid' : bid_price,
            'ask' : ask_price,
            'mid' : (bid_price + ask_price) / 2.0
        }
        instruments.append(option)
        
    return instruments
