Exemple #1
0
class BtfxWss:
    """Websocket Client Interface to Bitfinex WSS API

    It features separate threads for the connection and data handling.
    Data can be accessed using the provided methods.
    """
    def __init__(self,
                 key=None,
                 secret=None,
                 log_level=None,
                 logging_file_handler=None,
                 **wss_kwargs):
        """
        Initializes BtfxWss Instance.
        :param key: Api Key as string
        :param secret: Api secret as string
        :param addr: Websocket API Address
        """
        self.key = key if key else ''
        self.secret = secret if secret else ''
        self.subscribe_messages = set()

        # Set up book-keeping variables & configurations
        self.channel_configs = defaultdict(dict)

        self.conn = WebSocketConnection(log_level=log_level,
                                        on_reconnect=self._on_reconnect,
                                        **wss_kwargs)
        if logging_file_handler is not None:
            self.conn.log.addHandler(logging_file_handler)
        self.queue_processor = QueueProcessor(self.conn.q, log_level=log_level)

    ##############
    # Properties #
    ##############

    @property
    def orders(self):
        """Return queue containing open orders associated with the user account.
        
        :return: Queue()
        """
        return self.queue_processor.account['Orders']

    @property
    def positions(self):
        """Return queue containing open positions associated with the user 
        account.
        
        :return: Queue()
        """
        return self.queue_processor.account['Positions']

    @property
    def historical_orders(self):
        """Return history of orders associated with the user account.
        
        :return: Queue()
        """
        return self.queue_processor.account['Historical Orders']

    @property
    def transactions(self):
        """Return history of trades associacted with the user account.
        
        :return: Queue()
        """
        return self.queue_processor.account['Trades']

    @property
    def loans(self):
        """Return current loans associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Loans']

    @property
    def historical_loans(self):
        """Return history of loans associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Loans']

    @property
    def wallets(self):
        """Return wallet balances associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Wallets']

    @property
    def balance_info(self):
        """Return balance information associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Balance Info']

    @property
    def margin_info(self):
        """Return margin information associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Margin Info']

    @property
    def offers(self):
        """Return current offers associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Offers']

    @property
    def historical_offers(self):
        """Return history of offers associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Offers']

    @property
    def funding_info(self):
        """Return funding information associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Info']

    @property
    def credits(self):
        """Return current credits associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Credits']

    @property
    def historical_credits(self):
        """Return history of credits associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Credits']

    @property
    def channel_directory(self):
        """Return channel directory of currently subscribed channels.

        :return: Queue()
        """
        return self.queue_processor.channel_directory

    @property
    def funding_trades(self):
        """Return funding trades associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding_trades']

    @property
    def notifications(self):
        """Return notifications associacted with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Notifications']

    ##############################################
    # Client Initialization and Shutdown Methods #
    ##############################################

    def start(self):
        """Start the client.
        
        :return: 
        """
        self.conn.start()
        self.queue_processor.start()

    def stop(self):
        """Stop the client.
        
        :return: 
        """
        self.conn.disconnect()
        self.queue_processor.join()

    ##########################
    # Data Retrieval Methods #
    ##########################

    def raw_data(self):
        return self.queue_processor.raw_data

    def tickers(self, pair):
        """Return a queue containing all received ticker data.
        
        :param pair: 
        :return: Queue()
        """
        key = ('ticker', pair)
        if key in self.queue_processor.tickers:
            return self.queue_processor.tickers[key]
        else:
            raise KeyError(pair)

    def books(self, pair):
        """Return a queue containing all received book data.

        :param pair: 
        :return: Queue()
        """
        key = ('book', pair)
        if key in self.queue_processor.books:
            return self.queue_processor.books[key]
        else:
            raise KeyError(pair)

    def raw_books(self, pair):
        """Return a queue containing all received raw book data.

        :param pair: 
        :return: Queue()
        """
        key = ('raw_book', pair)
        if key in self.queue_processor.raw_books:
            return self.queue_processor.raw_books[key]
        else:
            raise KeyError(pair)

    def trades(self, pair):
        """Return a queue containing all received trades data.

        :param pair: 
        :return: Queue()
        """
        key = ('trades', pair)
        if key in self.queue_processor.trades:
            return self.queue_processor.trades[key]
        else:
            raise KeyError(pair)

    def candles(self, pair, timeframe=None):
        """Return a queue containing all received candles data.

        :param pair: str, Symbol pair to request data for
        :param timeframe: str
        :return: Queue()
        """
        timeframe = '1m' if not timeframe else timeframe
        key = ('candles', pair, timeframe)
        if key in self.queue_processor.candles:
            return self.queue_processor.candles[key]
        else:
            raise KeyError(pair)

    ##########################################
    # Subscription and Configuration Methods #
    ##########################################

    def _subscribe(self, channel_name, identifier, **kwargs):
        q = {'event': 'subscribe', 'channel': channel_name}
        q.update(**kwargs)
        log.debug("_subscribe: %s", q)
        self.subscribe_messages.add(json.dumps(q))
        self.conn.send(**q)
        self.channel_configs[identifier] = q

    def _unsubscribe(self, channel_name, identifier, **kwargs):

        channel_id = self.channel_directory[identifier]
        q = {'event': 'unsubscribe', 'chanId': channel_id}
        q.update(kwargs)
        self.conn.send(**q)
        self.channel_configs.pop(identifier)

    def _on_reconnect(self):
        for elem in self.subscribe_messages:
            self.conn.send(json.loads(elem))

    def config(self,
               decimals_as_strings=True,
               ts_as_dates=False,
               sequencing=False,
               **kwargs):
        """Send configuration to websocket server

        :param decimals_as_strings: bool, turn on/off decimals as strings
        :param ts_as_dates: bool, decide to request timestamps as dates instead
        :param sequencing: bool, turn on sequencing
        :param kwargs:
        :return:
        """
        flags = 0
        if decimals_as_strings:
            flags += 8
        if ts_as_dates:
            flags += 32
        if sequencing:
            flags += 65536
        q = {'event': 'conf', 'flags': flags}
        q.update(kwargs)
        self.conn.send(**q)

    @is_connected
    def subscribe_to_ticker(self, pair, **kwargs):
        """Subscribe to the passed pair's ticker channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('ticker', pair)
        self._subscribe('ticker', identifier, symbol=pair, **kwargs)

    @is_connected
    def unsubscribe_from_ticker(self, pair, **kwargs):
        """Unsubscribe to the passed pair's ticker channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('ticker', pair)
        self._unsubscribe('ticker', identifier, symbol=pair, **kwargs)

    @is_connected
    def subscribe_to_order_book(self, pair, **kwargs):
        """Subscribe to the passed pair's order book channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('book', pair)
        self._subscribe('book', identifier, symbol=pair, **kwargs)

    @is_connected
    def unsubscribe_from_order_book(self, pair, **kwargs):
        """Unsubscribe to the passed pair's order book channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('book', pair)
        self._unsubscribe('book', identifier, symbol=pair, **kwargs)

    @is_connected
    def subscribe_to_raw_order_book(self, pair, prec=None, **kwargs):
        """Subscribe to the passed pair's raw order book channel.

        :param pair: str, Symbol pair to request data for
        :param prec: 
        :param kwargs:
        :return:
        """
        identifier = ('raw_book', pair)
        prec = 'R0' if prec is None else prec
        self._subscribe('book', identifier, pair=pair, prec=prec, **kwargs)

    @is_connected
    def unsubscribe_from_raw_order_book(self, pair, prec=None, **kwargs):
        """Unsubscribe to the passed pair's raw order book channel.

        :param pair: str, Symbol pair to request data for
        :param prec: 
        :param kwargs:
        :return:
        """
        identifier = ('raw_book', pair)
        prec = 'R0' if prec is None else prec
        self._unsubscribe('book', identifier, pair=pair, prec=prec, **kwargs)

    @is_connected
    def subscribe_to_trades(self, pair, **kwargs):
        """Subscribe to the passed pair's trades channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('trades', pair)
        self._subscribe('trades', identifier, symbol=pair, **kwargs)

    @is_connected
    def unsubscribe_from_trades(self, pair, **kwargs):
        """Unsubscribe to the passed pair's trades channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('trades', pair)
        self._unsubscribe('trades', identifier, symbol=pair, **kwargs)

    @is_connected
    def subscribe_to_candles(self, pair, timeframe=None, **kwargs):
        """Subscribe to the passed pair's OHLC data channel.

        :param pair: str, Symbol pair to request data for
        :param timeframe: str, {1m, 5m, 15m, 30m, 1h, 3h, 6h, 12h,
                                1D, 7D, 14D, 1M}
        :param kwargs:
        :return:
        """

        valid_tfs = [
            '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D',
            '14D', '1M'
        ]
        if timeframe:
            if timeframe not in valid_tfs:
                raise ValueError("timeframe must be any of %s" % valid_tfs)
        else:
            timeframe = '1m'
        identifier = ('candles', pair, timeframe)
        pair = 't' + pair if not pair.startswith('t') else pair
        key = 'trade:' + timeframe + ':' + pair
        self._subscribe('candles', identifier, key=key, **kwargs)

    @is_connected
    def unsubscribe_from_candles(self, pair, timeframe=None, **kwargs):
        """Unsubscribe to the passed pair's OHLC data channel.

        v
        :param timeframe: str, {1m, 5m, 15m, 30m, 1h, 3h, 6h, 12h,
                                1D, 7D, 14D, 1M}
        :param kwargs:
        :return:
        """

        valid_tfs = [
            '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D',
            '14D', '1M'
        ]
        if timeframe:
            if timeframe not in valid_tfs:
                raise ValueError("timeframe must be any of %s" % valid_tfs)
        else:
            timeframe = '1m'
        identifier = ('candles', pair, timeframe)
        pair = 't' + pair if not pair.startswith('t') else pair
        key = 'trade:' + timeframe + ':' + pair

        self._unsubscribe('candles', identifier, key=key, **kwargs)

    @is_connected
    def authenticate(self):
        """Authenticate with the Bitfinex API.
        
        :return: 
        """
        if not self.key and not self.secret:
            raise ValueError("Must supply both key and secret key for API!")
        nonce = str(int(time.time() * 1000))
        auth_string = 'AUTH' + nonce
        auth_sig = hmac.new(self.secret.encode(), auth_string.encode(),
                            hashlib.sha384).hexdigest()

        payload = {
            'event': 'auth',
            'apiKey': self.key,
            'authSig': auth_sig,
            'authPayload': auth_string,
            'authNonce': nonce
        }
        self.conn.send(**payload)

    @is_connected
    def new_order(self, **order_settings):
        """Post a new Order va Websocket.

        :param kwargs:
        :return:
        """
        self._send_auth_command('on', order_settings)

    @is_connected
    def cancel_order(self, multi=False, **order_identifiers):
        """Cancel one or multiple orders via Websocket.

        :param multi: bool, whether order_settings contains settings for one, or
                      multiples orders
        :param order_identifiers: Identifiers for the order(s) you with to cancel
        :return:
        """
        if multi:
            self._send_auth_command('oc_multi', order_identifiers)
        else:
            self._send_auth_command('oc', order_identifiers)

    @is_connected
    def order_multi_op(self, *operations):
        """Execute multiple, order-related operations via Websocket.

        :param operations: operations to send to the websocket
        :return:
        """
        self._send_auth_command('ox_multi', operations)

    @is_connected
    def calc(self, *calculations):
        """Request one or several operations via Websocket.

        :param calculations: calculations as strings to send to the websocket
        :return:
        """
        self._send_auth_command('calc', calculations)

    def _send_auth_command(self, channel_name, data):
        payload = [0, channel_name, None, data]
        self.conn.send(list_data=payload)
Exemple #2
0
class BtfxWssClient:
    """Websocket Client Interface to Bitfinex WSS API

    It features separate threads for the connection and data handling.
    Data can be accessed using the provided methods.
    """
    def __init__(self, key=None, secret=None, **wss_kwargs):
        """
        Initializes BtfxWssClient Instance.
        :param key: Api Key as string
        :param secret: Api secret as string
        :param addr: Websocket API Address
        """
        self.key = key if key else ''
        self.secret = secret if secret else ''

        self.conn = WebSocketConnection(**wss_kwargs)
        self.queue_processor = QueueProcessor(self.conn.q)

    ##############
    # Properties #
    ##############
    @property
    def channel_configs(self):
        return self.conn.channel_configs

    @property
    def account(self):
        return self.queue_processor.account

    @property
    def orders(self):
        """Return queue containing open orders associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Orders']

    @property
    def orders_new(self):
        """Return queue containing new orders associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Order New']

    @property
    def orders_update(self):
        """Return queue containing order updates associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Order Update']

    @property
    def orders_cancel(self):
        """Return queue containing order cancellations associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Order Cancel']

    @property
    def positions(self):
        """Return queue containing open positions associated with the user

        account.
        :return: Queue()
        """
        return self.queue_processor.account['Positions']

    @property
    def positions_new(self):
        """Return queue containing new positions associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Position New']

    @property
    def positions_update(self):
        """Return queue containing position updates associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Position Update']

    @property
    def positions_cancel(self):
        """Return queue containing position cancellations associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Position Cancel']

    @property
    def funding_offer_new(self):
        """Return queue containing new funding offers associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Offer New']

    @property
    def funding_offer_update(self):
        """Return queue containing funding offer updates associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Offer Update']

    @property
    def funding_offer_cancel(self):
        """Return queue containing canceled funding offers associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Offer Cancel']

    @property
    def funding_credit_new(self):
        """Return queue containing new funding credit associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Credit New']

    @property
    def funding_credit_update(self):
        """Return queue containing funding credit updates associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Credit Update']

    @property
    def funding_credit_cancel(self):
        """Return queue containing canceled funding credit associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Credit Cancel']

    @property
    def funding_loan_new(self):
        """Return queue containing new funding loan associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Loan New']

    @property
    def funding_loan_update(self):
        """Return queue containing funding loan updates associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Loan Update']

    @property
    def funding_loan_cancel(self):
        """Return queue containing canceled funding loan associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Loan Cancel']

    @property
    def transactions(self):
        """Return history of trades associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Trades']

    @property
    def loans(self):
        """Return current loans associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Loans']

    @property
    def wallets(self):
        """Return wallet balances associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Wallets']

    @property
    def balance_info(self):
        """Return balance information associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Balance Info']

    @property
    def margin_info(self):
        """Return margin information associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Margin Info']

    @property
    def offers(self):
        """Return current offers associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Offers']

    @property
    def funding_info(self):
        """Return funding information associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding Info']

    @property
    def credits(self):
        """Return current credits associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Credits']

    @property
    def channel_directory(self):
        """Return channel directory of currently subscribed channels.

        :return: Queue()
        """
        return self.queue_processor.channel_directory

    @property
    def funding_trades(self):
        """Return funding trades associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Funding_trades']

    @property
    def notifications(self):
        """Return notifications associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Notifications']

    # DEPRECATED FUNCTIONS
    @property
    def historical_credits(self):
        """Return history of credits associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Credits']

    @property
    def historical_offers(self):
        """Return history of offers associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Offers']

    @property
    def historical_loans(self):
        """Return history of loans associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Loans']

    @property
    def historical_orders(self):
        """Return history of orders associated with the user account.

        :return: Queue()
        """
        return self.queue_processor.account['Historical Orders']

    ##############################################
    # Client Initialization and Shutdown Methods #
    ##############################################

    def start(self):
        """Start the client.

        :return:
        """
        self.conn.start()
        self.queue_processor.start()

    def stop(self):
        """Stop the client.

        :return:
        """
        self.conn.disconnect()
        self.queue_processor.join()

    def reset(self):
        """Reset the client.

        :return:
        """
        self.conn.reconnect()
        while not self.conn.connected.is_set():
            log.info("reset(): Waiting for connection to be set up..")
            time.sleep(1)

        for key in self.channel_configs:
            self.conn.send(**self.channel_configs[key])

    ##########################
    # Data Retrieval Methods #
    ##########################

    def tickers(self, pair):
        """Return a queue containing all received ticker data.

        :param pair:
        :return: Queue()
        """
        key = ('ticker', pair)
        return self.queue_processor.tickers[key]

    def books(self, pair):
        """Return a queue containing all received book data.

        :param pair:
        :return: Queue()
        """
        key = ('book', pair)
        return self.queue_processor.books[key]

    def raw_books(self, pair):
        """Return a queue containing all received raw book data.

        :param pair:
        :return: Queue()
        """
        key = ('raw_book', pair)
        return self.queue_processor.raw_books[key]

    def trades(self, pair):
        """Return a queue containing all received trades data.

        :param pair:
        :return: Queue()
        """
        key = ('trades', pair)
        return self.queue_processor.trades[key]

    def candles(self, pair, timeframe=None):
        """Return a queue containing all received candles data.

        :param pair: str, Symbol pair to request data for
        :param timeframe: str
        :return: Queue()
        """
        timeframe = '1m' if not timeframe else timeframe
        key = ('candles', pair, timeframe)
        return self.queue_processor.candles[key]

    ##########################################
    # Subscription and Configuration Methods #
    ##########################################

    def _subscribe(self, channel_name, identifier, **kwargs):
        q = {'event': 'subscribe', 'channel': channel_name}
        q.update(**kwargs)
        log.debug("_subscribe: %s", q)
        self.conn.send(**q)
        self.channel_configs[identifier] = q

    def _unsubscribe(self, channel_name, identifier, **kwargs):

        if identifier not in self.channel_directory:
            return

        channel_id = self.channel_directory[identifier]
        q = {'event': 'unsubscribe', 'chanId': channel_id}
        q.update(kwargs)
        self.conn.send(**q)
        self.channel_configs.pop(identifier)

    def config(self,
               decimals_as_strings=True,
               ts_as_dates=False,
               sequencing=False,
               **kwargs):
        """Send configuration to websocket server

        :param decimals_as_strings: bool, turn on/off decimals as strings
        :param ts_as_dates: bool, decide to request timestamps as dates instead
        :param sequencing: bool, turn on sequencing
        :param kwargs:
        :return:
        """
        flags = 0
        if decimals_as_strings:
            flags += 8
        if ts_as_dates:
            flags += 32
        if sequencing:
            flags += 65536
        q = {'event': 'conf', 'flags': flags}
        q.update(kwargs)
        self.conn.send(**q)

    @is_connected
    def subscribe_to_ticker(self, pair, **kwargs):
        """Subscribe to the passed pair's ticker channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('ticker', pair)
        self._subscribe('ticker', identifier, symbol=pair, **kwargs)

    @is_connected
    def unsubscribe_from_ticker(self, pair, **kwargs):
        """Unsubscribe to the passed pair's ticker channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('ticker', pair)
        self._unsubscribe('ticker', identifier, symbol=pair, **kwargs)

    @is_connected
    def subscribe_to_order_book(self, pair, **kwargs):
        """Subscribe to the passed pair's order book channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('book', pair)
        self._subscribe('book', identifier, symbol=pair, **kwargs)

    @is_connected
    def unsubscribe_from_order_book(self, pair, **kwargs):
        """Unsubscribe to the passed pair's order book channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('book', pair)
        self._unsubscribe('book', identifier, symbol=pair, **kwargs)

    @is_connected
    def subscribe_to_raw_order_book(self, pair, prec=None, **kwargs):
        """Subscribe to the passed pair's raw order book channel.

        :param pair: str, Symbol pair to request data for
        :param prec:
        :param kwargs:
        :return:
        """
        identifier = ('raw_book', pair)
        prec = 'R0' if prec is None else prec
        self._subscribe('book', identifier, pair=pair, prec=prec, **kwargs)

    @is_connected
    def unsubscribe_from_raw_order_book(self, pair, prec=None, **kwargs):
        """Unsubscribe to the passed pair's raw order book channel.

        :param pair: str, Symbol pair to request data for
        :param prec:
        :param kwargs:
        :return:
        """
        identifier = ('raw_book', pair)
        prec = 'R0' if prec is None else prec
        self._unsubscribe('book', identifier, pair=pair, prec=prec, **kwargs)

    @is_connected
    def subscribe_to_trades(self, pair, **kwargs):
        """Subscribe to the passed pair's trades channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('trades', pair)
        self._subscribe('trades', identifier, symbol=pair, **kwargs)

    @is_connected
    def unsubscribe_from_trades(self, pair, **kwargs):
        """Unsubscribe to the passed pair's trades channel.

        :param pair: str, Symbol pair to request data for
        :param kwargs:
        :return:
        """
        identifier = ('trades', pair)
        self._unsubscribe('trades', identifier, symbol=pair, **kwargs)

    @is_connected
    def subscribe_to_candles(self, pair, timeframe=None, **kwargs):
        """Subscribe to the passed pair's OHLC data channel.

        :param pair: str, Symbol pair to request data for
        :param timeframe: str, {1m, 5m, 15m, 30m, 1h, 3h, 6h, 12h,
                                1D, 7D, 14D, 1M}
        :param kwargs:
        :return:
        """

        valid_tfs = [
            '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D',
            '14D', '1M'
        ]
        if timeframe:
            if timeframe not in valid_tfs:
                raise ValueError("timeframe must be any of %s" % valid_tfs)
        else:
            timeframe = '1m'
        identifier = ('candles', pair, timeframe)
        pair = 't' + pair if not pair.startswith('t') else pair
        key = 'trade:' + timeframe + ':' + pair
        self._subscribe('candles', identifier, key=key, **kwargs)

    @is_connected
    def unsubscribe_from_candles(self, pair, timeframe=None, **kwargs):
        """Unsubscribe to the passed pair's OHLC data channel.

        :param timeframe: str, {1m, 5m, 15m, 30m, 1h, 3h, 6h, 12h,
                                1D, 7D, 14D, 1M}
        :param kwargs:
        :return:
        """

        valid_tfs = [
            '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D',
            '14D', '1M'
        ]
        if timeframe:
            if timeframe not in valid_tfs:
                raise ValueError("timeframe must be any of %s" % valid_tfs)
        else:
            timeframe = '1m'
        identifier = ('candles', pair, timeframe)
        pair = 't' + pair if not pair.startswith('t') else pair
        key = 'trade:' + timeframe + ':' + pair

        self._unsubscribe('candles', identifier, key=key, **kwargs)

    @is_connected
    def authenticate(self):
        """Authenticate with the Bitfinex API.

        :return:
        """
        if not self.key and not self.secret:
            raise ValueError("Must supply both key and secret key for API!")
        self.channel_configs['auth'] = {
            'api_key': self.key,
            'secret': self.secret
        }
        self.conn.send(api_key=self.key, secret=self.secret, auth=True)

    @is_connected
    def new_order(self, order_settings):
        """Post a new Order via Websocket.

        :param kwargs:
        :return:
        """
        self._send_auth_command('on', order_settings)

    @is_connected
    def cancel_order(self, multi=False, **order_identifiers):
        """Cancel one or multiple orders via Websocket.

        :param multi: bool, whether order_settings contains settings for one, or
                      multiples orders
        :param order_identifiers: Identifiers for the order(s) you with to cancel
        :return:
        """
        if multi:
            self._send_auth_command('oc_multi', order_identifiers)
        else:
            self._send_auth_command('oc', order_identifiers)

    @is_connected
    def order_multi_op(self, *operations):
        """Execute multiple, order-related operations via Websocket.

        :param operations: operations to send to the websocket
        :return:
        """
        self._send_auth_command('ox_multi', operations)

    @is_connected
    def calc(self, *calculations):
        """Request one or several operations via Websocket.

        :param calculations: calculations as strings to send to the websocket
        :return:
        """
        self._send_auth_command('calc', calculations)

    def _send_auth_command(self, channel_name, data):
        payload = [0, channel_name, None, data]
        self.conn.send(list_data=payload)
Exemple #3
0
class BtfxWss:
    """Websocket Client Interface to Bitfinex WSS API

    It features separate threads for the connection and data handling.
    Data can be accessed using the provided methods.
    """
    def __init__(self, key=None, secret=None, log_level=None, **wss_kwargs):
        """
        Initializes BtfxWss Instance.
        :param key: Api Key as string
        :param secret: Api secret as string
        :param addr: Websocket API Address
        """
        self.key = key if key else ''
        self.secret = secret if secret else ''

        # Set up book-keeping variables & configurations
        self.channel_configs = defaultdict(dict)  # Variables, as set by
        # subscribe command

        self.conn = WebSocketConnection(log_level=log_level, **wss_kwargs)
        self.queue_processor = QueueProcessor(self.conn.q, log_level=log_level)

    @property
    def channel_directory(self):
        return self.queue_processor.channel_directory

    def tickers(self, pair):
        key = ('ticker', pair)
        if key in self.queue_processor.tickers:
            return self.queue_processor.tickers[key]
        else:
            self.conn.disconnect()
            return None

    def books(self, pair):
        key = ('book', pair)
        if key in self.queue_processor.books:
            return self.queue_processor.books[key]
        else:
            raise KeyError(pair)

    def raw_books(self, pair):
        key = ('raw_book', pair)
        if key in self.queue_processor.raw_books:
            return self.queue_processor.raw_books[key]
        else:
            raise KeyError(pair)

    def trades(self, pair):
        key = ('trades', pair)
        if key in self.queue_processor.trades:
            return self.queue_processor.trades[key]
        else:
            raise KeyError(pair)

    def candles(self, pair, timeframe):
        key = ('candles', pair, timeframe)
        if key in self.queue_processor.candles:
            return self.queue_processor.candles[key]
        else:
            raise KeyError(pair)

    def start(self):
        self.conn.start()
        self.queue_processor.start()

    def stop(self, _log=None):
        if _log:
            log.debug(str(_log))
        self.conn.disconnect()
        self.queue_processor.join()

    def config(self,
               decimals_as_strings=True,
               ts_as_dates=False,
               sequencing=False,
               **kwargs):
        """Send configuration to websocket server

        :param decimals_as_strings: bool, turn on/off decimals as strings
        :param ts_as_dates: bool, decide to request timestamps as dates instead
        :param sequencing: bool, turn on sequencing
        :param kwargs:
        :return:
        """
        flags = 0
        if decimals_as_strings:
            flags += 8
        if ts_as_dates:
            flags += 32
        if sequencing:
            flags += 65536
        q = {'event': 'conf', 'flags': flags}
        q.update(kwargs)
        self.conn.send(**q)

    def _subscribe(self, channel_name, identifier, **kwargs):
        q = {'event': 'subscribe', 'channel': channel_name}
        q.update(**kwargs)
        log.debug("_subscribe: %s", q)
        try:
            self.conn.send(**q)
        except Exception as e:
            self.stop(str(e))

    def _unsubscribe(self, channel_name, identifier, **kwargs):

        channel_id = self.channel_directory[(channel_name, )]
        q = {'event': 'unsubscribe', 'chanId': channel_id}
        q.update(kwargs)
        self.conn.send(**q)
        self.channel_configs.pop(identifier)

    def subscribe_to_ticker(self, pair, unsubscribe=False, **kwargs):
        """Subscribe to the passed pair's ticker channel.

        :param pair: str, Pair to request data for.
        :param kwargs:
        :return:
        """
        identifier = ('ticker', pair)
        if unsubscribe:
            self._unsubscribe('ticker', identifier, symbol=pair, **kwargs)
        else:
            self._subscribe('ticker', identifier, symbol=pair, **kwargs)

    def subscribe_to_order_book(self, pair, unsubscribe=False, **kwargs):
        """Subscribe to the passed pair's order book channel.

        :param pair: str, Pair to request data for.
        :param kwargs:
        :return:
        """
        identifier = ('book', pair)
        if unsubscribe:
            self._unsubscribe('book', identifier, symbol=pair, **kwargs)
        else:
            self._subscribe('book', identifier, symbol=pair, **kwargs)

    def subscribe_to_raw_order_book(self,
                                    pair,
                                    prec=None,
                                    unsubscribe=False,
                                    **kwargs):
        """Subscribe to the passed pair's raw order book channel.

        :param pair: str, Pair to request data for.
        :param kwargs:
        :return:
        """
        identifier = ('raw_book', pair)
        prec = 'R0' if prec is None else prec
        if unsubscribe:
            self._unsubscribe('book',
                              identifier,
                              pair=pair,
                              prec=prec,
                              **kwargs)
        else:
            self._subscribe('book', identifier, pair=pair, prec=prec, **kwargs)

    def subscribe_to_trades(self, pair, unsubscribe=False, **kwargs):
        """Subscribe to the passed pair's trades channel.

        :param pair: str, Pair to request data for.
        :param kwargs:
        :return:
        """
        identifier = ('trades', pair)
        if unsubscribe:
            self._unsubscribe('trades', identifier, symbol=pair, **kwargs)
        else:
            self._subscribe('trades', identifier, symbol=pair, **kwargs)

    def subscribe_to_candles(self,
                             pair,
                             timeframe=None,
                             unsubcribe=False,
                             **kwargs):
        """Subscribe to the passed pair's OHLC data channel.

        :param pair: str, Pair to request data for.
        :param timeframe: str, {1m, 5m, 15m, 30m, 1h, 3h, 6h, 12h,
                                1D, 7D, 14D, 1M}
        :param kwargs:
        :return:
        """

        valid_tfs = [
            '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D',
            '14D', '1M'
        ]
        if timeframe:
            if timeframe not in valid_tfs:
                raise ValueError("timeframe must be any of %s" % valid_tfs)
        else:
            timeframe = '1m'
        identifier = ('candles', pair, timeframe)
        pair = 't' + pair if not pair.startswith('t') else pair
        key = 'trade:' + timeframe + ':' + pair
        if unsubcribe:
            self._unsubscribe('candles', identifier, key=key, **kwargs)
        else:
            self._subscribe('candles', identifier, key=key, **kwargs)