Exemple #1
0
class KrakenFetcher(Fetcher):
    """
    Kraken market data fetcher.
    """

    TF_MAP = {
        60: 1,  # 1m
        300: 5,  # 5m
        900: 15,  # 15m
        1800: 30,  # 30m
        3600: 60,  # 1h
        14400: 240,  # 4h
        86400.0: 1440,  # 1d
        # 604800: 10080,  # 1w (not allowed because starts on thuesday)
        # 1296000: 21600  # 15d
    }

    def __init__(self, service):
        super().__init__("kraken.com", service)

        self._connector = None

    def connect(self):
        super().connect()

        try:
            identity = self.service.identity(self._name)

            if identity:
                if not self._connector:
                    self._connector = Connector(self.service,
                                                identity.get('api-key'),
                                                identity.get('api-secret'), [],
                                                identity.get('host'))

                if not self._connector.connected:
                    self._connector.connect(use_ws=False)

                #
                # instruments
                #

                # get all products symbols
                self._available_instruments = set()

                instruments = self._connector.instruments()

                for market_id, instrument in instruments.items():
                    self._available_instruments.add(market_id)

        except Exception as e:
            logger.error(repr(e))
            error_logger.error(traceback.format_exc())

    def disconnect(self):
        super().disconnect()

        try:
            if self._connector:
                self._connector.disconnect()
                self._connector = None
        except Exception as e:
            logger.error(repr(e))
            error_logger.error(traceback.format_exc())

    @property
    def connector(self):
        return self._connector

    @property
    def connected(self):
        return self._connector is not None and self._connector.connected

    @property
    def authenticated(self):
        return self._connector and self._connector.authenticated

    def fetch_trades(self,
                     market_id,
                     from_date=None,
                     to_date=None,
                     n_last=None):
        trades = []
        # get all trades and append them into a file
        try:
            trades = self._connector.get_historical_trades(
                market_id, from_date, to_date)
        except Exception as e:
            logger.error(
                "Fetcher %s cannot retrieve aggregated trades on market %s" %
                (self.name, market_id))

        count = 0

        for trade in trades:
            count += 1
            # timestamp, bid, ofr, volume
            yield (trade)

        logger.info(
            "Fetcher %s has retrieved on market %s %s aggregated trades" %
            (self.name, market_id, count))

    def fetch_candles(self,
                      market_id,
                      timeframe,
                      from_date=None,
                      to_date=None,
                      n_last=None):
        if timeframe not in self.TF_MAP:
            logger.error("Fetcher %s does not support timeframe %s" %
                         (self.name, timeframe))
            return

        candles = []

        # second timeframe to kraken interval
        interval = self.TF_MAP[timeframe]

        try:
            candles = self._connector.get_historical_candles(
                market_id, interval, from_date, to_date)
        except Exception as e:
            logger.error("Fetcher %s cannot retrieve candles %s on market %s" %
                         (self.name, interval, market_id))
            error_logger.error(traceback.format_exc())

        count = 0

        for candle in candles:
            count += 1
            # store (timestamp, open bid, high bid, low bid, close bid, open ofr, high ofr, low ofr, close ofr, volume)
            if candle[0] is not None and candle[1] is not None and candle[
                    2] is not None and candle[3] is not None:
                yield ((candle[0], candle[1], candle[2], candle[3], candle[4],
                        candle[1], candle[2], candle[3], candle[4], candle[5]))

        logger.info(
            "Fetcher %s has retrieved on market %s %s candles for timeframe %s"
            % (self.name, market_id, count, interval))
Exemple #2
0
class KrakenWatcher(Watcher):
    """
    Kraken market watcher using REST + WS.

    @todo complete
    """
    def __init__(self, service):
        super().__init__("kraken.com", service,
                         Watcher.WATCHER_PRICE_AND_VOLUME)

        self._connector = None
        self._depths = {}  # depth chart per symbol tuple (last_id, bids, ofrs)

        self._acount_data = {}
        self._symbols_data = {}
        self._tickers_data = {}

        self._last_trade_id = {}

        self._assets = {}
        self._instruments = {}
        self._wsname_lookup = {}

    def connect(self):
        super().connect()

        try:
            self.lock()
            self._ready = False

            identity = self.service.identity(self._name)

            if identity:
                if not self._connector:
                    self._connector = Connector(self.service,
                                                identity.get('api-key'),
                                                identity.get('api-secret'),
                                                identity.get('host'))

                if not self._connector.connected or not self._connector.ws_connected:
                    self._connector.connect()

                # assets
                self._assets = self._connector.assets()

                # {'ADA': {'aclass': 'currency', 'altname': 'ADA', 'decimals': 8, 'display_decimals': 6}, 'ATOM': {'aclass': 'currency', 'altname': 'ATOM', 'decimals': 8, 'display_decimals': 6}, 'BAT': {'aclass': 'currency', 'altname': 'BAT', 'decimals': 10, 'display_decimals': 5}, 'BCH': {'aclass': 'currency', 'altname': 'BCH', 'decimals': 10, 'display_decimals': 5}, 'BSV': {'aclass': 'currency', 'altname': 'BSV', 'decimals': 10, 'display_decimals': 5}, 'DASH': {'aclass': 'currency', 'altname': 'DASH', 'decimals': 10, 'display_decimals': 5}, 'EOS': {'aclass': 'currency', 'altname': 'EOS', 'decimals': 10, 'display_decimals': 5}, 'GNO': {'aclass': 'currency', 'altname': 'GNO', 'decimals': 10, 'display_decimals': 5}, 'KFEE': {'aclass': 'currency', 'altname': 'FEE', 'decimals': 2, 'display_decimals': 2}, 'QTUM': {'aclass': 'currency', 'altname': 'QTUM', 'decimals': 10, 'display_decimals': 6}, 'USDT': {'aclass': 'currency', 'altname': 'USDT', 'decimals': 8, 'display_decimals': 4}, 'WAVES': {'aclass': 'currency', 'altname': 'WAVES', 'decimals': 10, 'display_decimals': 5}, 'XETC': {'aclass': 'currency', 'altname': 'ETC', 'decimals': 10, 'display_decimals': 5}, 'XETH': {'aclass': 'currency', 'altname': 'ETH', 'decimals': 10, 'display_decimals': 5}, 'XLTC': {'aclass': 'currency', 'altname': 'LTC', 'decimals': 10, 'display_decimals': 5}, 'XMLN': {'aclass': 'currency', 'altname': 'MLN', 'decimals': 10, 'display_decimals': 5}, 'XREP': {'aclass': 'currency', 'altname': 'REP', 'decimals': 10, 'display_decimals': 5}, 'XTZ': {'aclass': 'currency', 'altname': 'XTZ', 'decimals': 8, 'display_decimals': 5}, 'XXBT': {'aclass': 'currency', 'altname': 'XBT', 'decimals': 10, 'display_decimals': 5}, 'XXDG': {'aclass': 'currency', 'altname': 'XDG', 'decimals': 8, 'display_decimals': 2}, 'XXLM': {'aclass': 'currency', 'altname': 'XLM', 'decimals': 8, 'display_decimals': 5}, 'XXMR': {'aclass': 'currency', 'altname': 'XMR', 'decimals': 10, 'display_decimals': 5}, 'XXRP': {'aclass': 'currency', 'altname': 'XRP', 'decimals': 8, 'display_decimals': 5}, 'XZEC': {'aclass': 'currency', 'altname': 'ZEC', 'decimals': 10, 'display_decimals': 5}, 'ZCAD': {'aclass': 'currency', 'altname': 'CAD', 'decimals': 4, 'display_decimals': 2}, 'ZEUR': {'aclass': 'currency', 'altname': 'EUR', 'decimals': 4, 'display_decimals': 2}, 'ZGBP': {'aclass': 'currency', 'altname': 'GBP', 'decimals': 4, 'display_decimals': 2}, 'ZJPY': {'aclass': 'currency', 'altname': 'JPY', 'decimals': 2, 'display_decimals': 0}, 'ZUSD': {'aclass': 'currency', 'altname': 'USD', 'decimals': 4, 'display_decimals': 2}}

                for asset_name, details in self._assets.items():
                    pass
                    # {'aclass': 'currency', 'altname': 'ADA', 'decimals': 8, 'display_decimals': 6},

                #
                # instruments
                #

                # get all products symbols
                self._available_instruments = set()

                # prefetch all markets data with a single request to avoid one per market
                self.__prefetch_markets()

                instruments = self._instruments
                configured_symbols = self.configured_symbols()
                matching_symbols = self.matching_symbols_set(
                    configured_symbols, [
                        instrument
                        for instrument in list(self._instruments.keys())
                    ])

                pairs = []

                for market_id, instrument in instruments.items():
                    self._available_instruments.add(market_id)

                    # and watch it if configured or any
                    if market_id in matching_symbols:
                        # live data
                        pairs.append(instrument['wsname'])
                        self._wsname_lookup[instrument['wsname']] = market_id

                        # one more watched instrument
                        self.insert_watched_instrument(market_id, [0])

                if pairs:
                    self._connector.ws.subscribe_public(
                        subscription={'name': 'ticker'},
                        pair=pairs,
                        callback=self.__on_ticker_data)

                    self._connector.ws.subscribe_public(
                        subscription={'name': 'trade'},
                        pair=pairs,
                        callback=self.__on_trade_data)

                    # @todo see later
                    # self._connector.ws.subscribe_public(
                    #     subscription={
                    #         'name': 'book'
                    #     },
                    #     pair=pairs,
                    #     depth=10,  # 10 25 100 500 1000
                    #     callback=self.__on_depth_data
                    # )

                # and start ws manager
                self._connector.ws.start()

                # once market are init
                self._ready = True

        except Exception as e:
            logger.debug(repr(e))
            error_logger.error(traceback.format_exc())
        finally:
            self.unlock()

        if self._ready and self._connector and self._connector.connected:
            self.service.notify(Signal.SIGNAL_WATCHER_CONNECTED, self.name,
                                time.time())

    def disconnect(self):
        super().disconnect()

        try:
            self.lock()

            if self._connector:
                self._connector.disconnect()
                self._connector = None

            self._ready = False

        except Exception as e:
            logger.debug(repr(e))
            error_logger.error(traceback.format_exc())
        finally:
            self.unlock()

    @property
    def connector(self):
        return self._connector

    @property
    def connected(self):
        return elf._connector is not None and self._connector.connected and self._connector.ws_connected

    @property
    def authenticated(self):
        return self._connector and self._connector.authenticated

    def pre_update(self):
        if not self._ready or self._connector is None or not self._connector.connected or not self._connector.ws_connected:
            # retry in 2 second
            self._ready = False
            self._connector = None

            time.sleep(2)
            self.connect()
            return

    def update(self):
        if not super().update():
            return False

        if not self.connected:
            return False

        #
        # ohlc close/open
        #

        self.lock()
        self.update_from_tick()
        self.unlock()

        #
        # market info update (each 4h)
        #

        if time.time(
        ) - self._last_market_update >= KrakenWatcher.UPDATE_MARKET_INFO_DELAY:  # only once per 4h
            self.update_markets_info()
            self._last_market_update = time.time()

        return True

    def post_update(self):
        super().post_update()
        time.sleep(0.0005)

    def post_run(self):
        super().post_run()

    def fetch_market(self, market_id):
        """
        Fetch and cache it. It rarely changes.
        """
        instrument = self._instruments.get(market_id)

        market = None

        if instrument:
            market = Market(market_id, instrument['altname'])

            market.is_open = True
            market.expiry = '-'

            # "wsname":"XBT\/USD"
            # wsname = WebSocket pair name (if available)
            # pair_decimals = scaling decimal places for pair
            # lot_decimals = scaling decimal places for volume
            # lot_multiplier = amount to multiply lot volume by to get currency volume

            # "aclass_base":"currency"
            base_asset = instrument['base']  # XXBT
            market.set_base(base_asset, base_asset,
                            instrument['pair_decimals'])

            # "aclass_quote":"currency"
            quote_asset = instrument['quote']  # ZUSD
            market.set_quote(quote_asset, quote_asset,
                             instrument['lot_decimals'])  # 8

            # tick size at the base asset precision
            market.one_pip_means = math.pow(10.0,
                                            -instrument['pair_decimals'])  # 1
            market.value_per_pip = 1.0
            market.contract_size = 1.0
            market.lot_size = 1.0  # "lot":"unit", "lot_multiplier":1

            # "margin_call":80, "margin_stop":40
            # margin_call = margin call level
            # margin_stop = stop-out/liquidation margin level

            leverages = set(instrument.get('leverage_buy', []))
            leverages.intersection(set(instrument.get('leverage_sell', [])))

            market.margin_factor = 1.0 / max(leverages)

            market.set_leverages(leverages)

            size_limits = ["1.0", "0.0", "1.0"]
            notional_limits = ["1.0", "0.0", "0.0"]
            price_limits = ["0.0", "0.0", "0.0"]

            # size min/max/step @todo using lot_decimals / pair_decimals
            # for afilter in instrument["filters"]:
            #     if afilter['filterType'] == "LOT_SIZE":
            #         size_limits = [afilter['minQty'], afilter['maxQty'], afilter['stepSize']]

            #     elif afilter['filterType'] == "MIN_NOTIONAL":
            #         notional_limits[0] = afilter['minNotional']

            #     elif afilter['filterType'] == "PRICE_FILTER":
            #         price_limits = [afilter['minPrice'], afilter['maxPrice'], afilter['tickSize']]

            # if float(size_limits[2]) < 1:
            #     size_limits[2] = str(float(size_limits[2]))  # * 10)

            market.set_size_limits(float(size_limits[0]),
                                   float(size_limits[1]),
                                   float(size_limits[2]))
            market.set_price_limits(float(price_limits[0]),
                                    float(price_limits[1]),
                                    float(price_limits[2]))
            market.set_notional_limits(float(notional_limits[0]), 0.0, 0.0)

            # "lot":"unit"
            market.unit_type = Market.UNIT_AMOUNT
            market.market_type = Market.TYPE_CRYPTO
            market.trade = Market.TRADE_ASSET
            market.contract_type = Market.CONTRACT_SPOT

            # @todo add a copy with
            market.trade = Market.TRADE_MARGIN
            market.contract_type = Market.CONTRACT_FUTUR

            # orders capacities
            market.orders = Order.ORDER_LIMIT | Order.ORDER_MARKET | Order.ORDER_STOP | Order.ORDER_TAKE_PROFIT

            # "fees":[[0,0.26],[50000,0.24],[100000,0.22],[250000,0.2],[500000,0.18],[1000000,0.16],[2500000,0.14],[5000000,0.12],[10000000,0.1]],
            # "fees_maker":[[0,0.16],[50000,0.14],[100000,0.12],[250000,0.1],[500000,0.08],[1000000,0.06],[2500000,0.04],[5000000,0.02],[10000000,0]],

            # market.maker_fee = account['makerCommission'] * 0.0001
            # market.taker_fee = account['takerCommission'] * 0.0001

            # 'fee_volume_currency': 'ZUSD'
            # fee_volume_currency = volume discount currency

            # if quote_asset != self.BASE_QUOTE:
            #     if self._tickers_data.get(quote_asset+self.BASE_QUOTE):
            #         market.base_exchange_rate = float(self._tickers_data.get(quote_asset+self.BASE_QUOTE, {'price', '1.0'})['price'])
            #     elif self._tickers_data.get(self.BASE_QUOTE+quote_asset):
            #         market.base_exchange_rate = 1.0 / float(self._tickers_data.get(self.BASE_QUOTE+quote_asset, {'price', '1.0'})['price'])
            #     else:
            #         market.base_exchange_rate = 1.0
            # else:
            #     market.base_exchange_rate = 1.0

            # market.contract_size = 1.0 / mid_price
            # market.value_per_pip = market.contract_size / mid_price

            # volume 24h : not have here

            # store the last market info to be used for backtesting
            if not self._read_only:
                Database.inst().store_market_info((
                    self.name,
                    market.market_id,
                    market.symbol,
                    market.market_type,
                    market.unit_type,
                    market.contract_type,  # type
                    market.trade,
                    market.orders,  # type
                    market.base,
                    market.base_display,
                    market.base_precision,  # base
                    market.quote,
                    market.quote_display,
                    market.quote_precision,  # quote
                    market.expiry,
                    int(market.last_update_time * 1000.0),  # expiry, timestamp
                    str(market.lot_size),
                    str(market.contract_size),
                    str(market.base_exchange_rate),
                    str(market.value_per_pip),
                    str(market.one_pip_means),
                    '-',
                    *size_limits,
                    *notional_limits,
                    *price_limits,
                    str(market.maker_fee),
                    str(market.taker_fee),
                    str(market.maker_commission),
                    str(market.taker_commission)))

            # notify for strategy
            self.service.notify(Signal.SIGNAL_MARKET_INFO_DATA, self.name,
                                (market_id, market))

        return market

    def fetch_order_book(self, market_id):
        # https://api.kraken.com/0/public/Depth
        pass

    #
    # protected
    #

    def __prefetch_markets(self):
        self._assets = self._connector.assets()
        self._instruments = self._connector.instruments()

    def __on_depth_data(self, data):
        # @ref https://www.kraken.com/en-us/features/websocket-api#message-book
        pass

    def __on_ticker_data(self, data):
        if isinstance(data, list) and data[2] == "ticker":
            market_id = self._wsname_lookup.get(data[3])
            base_asset, quote_asset = data[3].split('/')

            if not market_id:
                return

            last_update_time = time.time()
            ticker = data[1]

            bid = float(ticker['b'][0])
            ofr = float(ticker['a'][0])

            vol24_base = float(ticker['v'][0])
            vol24_quote = float(ticker['v'][0]) * float(ticker['p'][0])

            # @todo compute base_exchange_rate
            if quote_asset != self.account.currency:
                if quote_asset in self._assets:
                    pass  # @todo direct or indirect
                else:
                    market.base_exchange_rate = 1.0  # could be EURUSD... but we don't have
            else:
                market.base_exchange_rate = 1.0

            market_data = (market_id, last_update_time > 0, last_update_time,
                           bid, ofr, None, None, None, vol24_base, vol24_quote)
            self.service.notify(Signal.SIGNAL_MARKET_DATA, self.name,
                                market_data)

        elif isinstance(data, dict):
            if data['event'] == "subscriptionStatus" and data[
                    'channelName'] == "ticker":
                # @todo register channelID...
                # {'channelID': 93, 'channelName': 'trade', 'event': 'subscriptionStatus', 'pair': 'ETH/USD', 'status': 'subscribed', 'subscription': {'name': 'ticker'}}
                pass

    def __on_trade_data(self, data):
        if isinstance(data, list) and data[2] == "trade":
            market_id = self._wsname_lookup.get(data[3])

            if not market_id:
                return

            for trade in data[1]:
                bid = float(trade[0])
                ofr = float(trade[0])
                vol = float(trade[1])
                trade_time = float(trade[2])
                # side = trade[3]

                tick = (trade_time, bid, ofr, vol)

                # store for generation of OHLCs
                self.lock()
                self._last_tick[market_id] = tick
                self.unlock()

                self.service.notify(Signal.SIGNAL_TICK_DATA, self.name,
                                    (market_id, tick))

                if not self._read_only:
                    Database.inst().store_market_trade(
                        (self.name, market_id, int(trade_time * 1000.0),
                         trade[0], trade[0], trade[1]))

                for tf in Watcher.STORED_TIMEFRAMES:
                    # generate candle per timeframe
                    self.lock()
                    candle = self.update_ohlc(market_id, tf, trade_time, bid,
                                              ofr, vol)
                    self.unlock()

                    if candle is not None:
                        self.service.notify(Signal.SIGNAL_CANDLE_DATA,
                                            self.name, (market_id, candle))

        elif isinstance(data, dict):
            if data['event'] == "subscriptionStatus" and data[
                    'channelName'] == "trade":
                # @todo register channelID...
                # {'channelID': 93, 'channelName': 'trade', 'event': 'subscriptionStatus', 'pair': 'ETH/USD', 'status': 'subscribed', 'subscription': {'name': 'trade'}}
                pass

    def __on_kline_data(self, data):
        pass

    def __on_user_data(self, data):
        pass

    #
    # miscs
    #

    def price_history(self, market_id, timestamp):
        """
        Retrieve the historical price for a specific market id.
        """
        pass

    def update_markets_info(self):
        """
        Update market info.
        """
        self.__prefetch_markets()

        for market_id in self._watched_instruments:
            market = self.fetch_market(market_id)

            if market.is_open:
                market_data = (market_id, market.is_open,
                               market.last_update_time, market.bid, market.ofr,
                               market.base_exchange_rate, market.contract_size,
                               market.value_per_pip, market.vol24h_base,
                               market.vol24h_quote)
            else:
                market_data = (market_id, market.is_open,
                               market.last_update_time, 0.0, 0.0, None, None,
                               None, None, None)

            self.service.notify(Signal.SIGNAL_MARKET_DATA, self.name,
                                market_data)

    def fetch_candles(self,
                      market_id,
                      timeframe,
                      from_date=None,
                      to_date=None,
                      n_last=None):
        pass  # @todo
Exemple #3
0
class KrakenWatcher(Watcher):
    """
    Kraken market watcher using REST + WS.

    @todo create order, cancel order, position info
    @todo contract_size, value_per_pip, base_exchange_rate (initials and updates)
    @todo fee from 30 day traded volume
    @todo order book WS
    """

    BASE_QUOTE = "ZUSD"

    TF_MAP = {
        60: 1,  # 1m
        300: 5,  # 5m
        900: 15,  # 15m
        1800: 30,  # 30m
        3600: 60,  # 1h
        14400: 240,  # 4h
        86400.0: 1440,  # 1d
        # 604800: 10080,  # 1w (not allowed because starts on thuesday)
        # 1296000: 21600  # 15d
    }

    def __init__(self, service):
        super().__init__("kraken.com", service,
                         Watcher.WATCHER_PRICE_AND_VOLUME)

        self._connector = None
        self._depths = {}  # depth chart per symbol tuple (last_id, bids, ofrs)

        self._acount_data = {}
        self._symbols_data = {}
        self._tickers_data = {}

        self._last_trade_id = {}

        self.__configured_symbols = set()  # cache for configured symbols set
        self.__matching_symbols = set()  # cache for matching symbols
        self.__ws_symbols = set()  # cache for matching symbols WS names

        self._assets = {}
        self._instruments = {}
        self._wsname_lookup = {}

    def connect(self):
        super().connect()

        with self._mutex:
            try:
                self._ready = False
                self._connecting = True

                identity = self.service.identity(self._name)

                if identity:
                    if not self._connector:
                        self._connector = Connector(
                            self.service, identity.get('account-id', ""),
                            identity.get('api-key'),
                            identity.get('api-secret'), identity.get('host'))

                    if not self._connector.connected or not self._connector.ws_connected:
                        self._connector.connect()

                    if self._connector and self._connector.connected:
                        #
                        # assets
                        #

                        self._assets = self._connector.assets()

                        for asset_name, details in self._assets.items():
                            # {'aclass': 'currency', 'altname': 'ADA', 'decimals': 8, 'display_decimals': 6},
                            pass

                        #
                        # instruments
                        #

                        # get all products symbols
                        self._available_instruments = set()

                        # prefetch all markets data with a single request to avoid one per market
                        self.__prefetch_markets()

                        instruments = self._instruments
                        configured_symbols = self.configured_symbols()
                        matching_symbols = self.matching_symbols_set(
                            configured_symbols, [
                                instrument for instrument in list(
                                    self._instruments.keys())
                            ])

                        # cache them
                        self.__configured_symbols = configured_symbols
                        self.__matching_symbols = matching_symbols

                        for market_id, instrument in instruments.items():
                            self._available_instruments.add(market_id)

                        #
                        # user data
                        #

                        ws_token = self._connector.get_ws_token()

                        if ws_token and ws_token.get('token'):
                            self._connector.ws.subscribe_private(
                                subscription={
                                    'name': 'ownTrades',
                                    'token': ws_token['token']
                                },
                                callback=self.__on_own_trades)

                            self._connector.ws.subscribe_private(
                                subscription={
                                    'name': 'openOrders',
                                    'token': ws_token['token']
                                },
                                callback=self.__on_open_orders)

                        # and start ws manager if necessarry
                        try:
                            self._connector.ws.start()
                        except RuntimeError:
                            logger.debug("%s WS already started..." %
                                         (self.name))

                        # once market are init
                        self._ready = True
                        self._connecting = False

                        logger.debug("%s connection successed" % (self.name))

            except Exception as e:
                error_logger.error(repr(e))
                traceback_logger.error(traceback.format_exc())

        if self._ready and self._connector and self._connector.connected:
            self.service.notify(Signal.SIGNAL_WATCHER_CONNECTED, self.name,
                                time.time())

    def disconnect(self):
        super().disconnect()

        with self._mutex:
            try:
                if self._connector:
                    self._connector.disconnect()
                    self._connector = None

                self._ready = False
            except Exception as e:
                error_logger.error(repr(e))
                traceback_logger.error(traceback.format_exc())

    @property
    def connector(self):
        return self._connector

    @property
    def connected(self):
        return self._connector is not None and self._connector.connected and self._connector.ws_connected

    @property
    def authenticated(self):
        return self._connector and self._connector.authenticated

    #
    # instruments
    #

    def subscribe(self,
                  market_id,
                  timeframe,
                  ohlc_depths=None,
                  order_book_depth=None):
        result = False
        with self._mutex:
            try:
                if market_id not in self.__matching_symbols:
                    return False

                instrument = self._instruments.get(market_id)
                if not instrument:
                    return False

                pairs = []

                # live data
                pairs.append(instrument['wsname'])
                self._wsname_lookup[instrument['wsname']] = market_id

                # fetch from 1m to 1w
                if self._initial_fetch:
                    self.fetch_and_generate(market_id, Instrument.TF_1M,
                                            self.DEFAULT_PREFETCH_SIZE * 3,
                                            Instrument.TF_3M)
                    self.fetch_and_generate(market_id, Instrument.TF_5M,
                                            self.DEFAULT_PREFETCH_SIZE, None)
                    self.fetch_and_generate(market_id, Instrument.TF_15M,
                                            self.DEFAULT_PREFETCH_SIZE * 2,
                                            Instrument.TF_30M)
                    self.fetch_and_generate(market_id, Instrument.TF_1H,
                                            self.DEFAULT_PREFETCH_SIZE * 2,
                                            Instrument.TF_2H)
                    self.fetch_and_generate(market_id, Instrument.TF_4H,
                                            self.DEFAULT_PREFETCH_SIZE, None)
                    self.fetch_and_generate(market_id, Instrument.TF_1D,
                                            self.DEFAULT_PREFETCH_SIZE * 7,
                                            Instrument.TF_1W)

                    logger.info("%s prefetch for %s" % (self.name, market_id))

                # one more watched instrument
                self.insert_watched_instrument(market_id, [0])

                if pairs:
                    self._connector.ws.subscribe_public(
                        subscription={'name': 'ticker'},
                        pair=pairs,
                        callback=self.__on_ticker_data)

                    self._connector.ws.subscribe_public(
                        subscription={'name': 'trade'},
                        pair=pairs,
                        callback=self.__on_trade_data)

                    if order_book_depth and order_book_depth in (10, 25, 100,
                                                                 500, 1000):
                        self._connector.ws.subscribe_public(
                            subscription={'name': 'book'},
                            pair=pairs,
                            depth=order_book_depth,
                            callback=self.__on_depth_data)

                    result = True

            except Exception as e:
                error_logger.error(repr(e))

        return result

    def unsubscribe(self, market_id, timeframe):
        with self._mutex:
            if market_id in self._multiplex_handlers:
                self._multiplex_handlers[market_id].close()
                del self._multiplex_handlers[market_id]

                return True

        return False

    #
    # processing
    #

    def pre_update(self):
        if not self._ready or self._connector is None or not self._connector.connected or not self._connector.ws_connected:
            # retry in 2 second
            self._ready = False
            self._connector = None

            time.sleep(2)
            self.connect()
            return

    def update(self):
        if not super().update():
            return False

        if not self.connected:
            return False

        #
        # ohlc close/open
        #

        with self._mutex:
            self.update_from_tick()

        #
        # market info update (each 4h)
        #

        if time.time(
        ) - self._last_market_update >= KrakenWatcher.UPDATE_MARKET_INFO_DELAY:  # only once per 4h
            try:
                self.update_markets_info()
                self._last_market_update = time.time()
            except Exception as e:
                error_logger.error("update_update_markets_info %s" % str(e))

        return True

    def post_update(self):
        super().post_update()
        time.sleep(0.0005)

    def post_run(self):
        super().post_run()

    def fetch_market(self, market_id):
        """
        Fetch and cache it. It rarely changes.
        """
        instrument = self._instruments.get(market_id)

        market = None

        if instrument:
            market = Market(market_id, instrument['altname'])

            market.is_open = True
            market.expiry = '-'

            # "wsname":"XBT\/USD"
            # wsname = WebSocket pair name (if available)
            # pair_decimals = scaling decimal places for pair
            # lot_decimals = scaling decimal places for volume
            # lot_multiplier = amount to multiply lot volume by to get currency volume

            # "aclass_base":"currency"
            base_asset = instrument['base']  # XXBT
            market.set_base(base_asset, base_asset,
                            instrument['pair_decimals'])

            # "aclass_quote":"currency"
            quote_asset = instrument['quote']  # ZUSD
            market.set_quote(quote_asset, quote_asset,
                             instrument['lot_decimals'])  # 8

            # tick size at the base asset precision
            market.one_pip_means = math.pow(10.0,
                                            -instrument['pair_decimals'])  # 1
            market.value_per_pip = 1.0
            market.contract_size = 1.0
            market.lot_size = 1.0  # "lot":"unit", "lot_multiplier":1

            # "margin_call":80, "margin_stop":40
            # margin_call = margin call level
            # margin_stop = stop-out/liquidation margin level

            leverages = set(instrument.get('leverage_buy', []))
            leverages.intersection(set(instrument.get('leverage_sell', [])))

            market.margin_factor = 1.0 / max(leverages) if len(
                leverages) > 0 else 1.0

            market.set_leverages(leverages)

            size_limit = self._size_limits.get(instrument['altname'], {})
            min_size = size_limit.get('min-size', 1.0)

            size_limits = [str(min_size), "0.0", str(min_size)]
            notional_limits = ["0.0", "0.0", "0.0"]
            price_limits = ["0.0", "0.0", "0.0"]

            market.set_size_limits(float(size_limits[0]),
                                   float(size_limits[1]),
                                   float(size_limits[2]))
            market.set_price_limits(float(price_limits[0]),
                                    float(price_limits[1]),
                                    float(price_limits[2]))
            market.set_notional_limits(float(notional_limits[0]), 0.0, 0.0)

            # "lot":"unit"
            market.unit_type = Market.UNIT_AMOUNT
            market.market_type = Market.TYPE_CRYPTO
            market.contract_type = Market.CONTRACT_SPOT

            market.trade = Market.TRADE_ASSET
            if leverages:
                market.trade |= Market.TRADE_MARGIN
                market.trade |= Market.TRADE_FIFO

            # orders capacities
            market.orders = Order.ORDER_LIMIT | Order.ORDER_MARKET | Order.ORDER_STOP | Order.ORDER_TAKE_PROFIT

            # @todo take the first but it might depends of the traded volume per 30 days, then request volume window to got it
            # "fees":[[0,0.26],[50000,0.24],[100000,0.22],[250000,0.2],[500000,0.18],[1000000,0.16],[2500000,0.14],[5000000,0.12],[10000000,0.1]],
            # "fees_maker":[[0,0.16],[50000,0.14],[100000,0.12],[250000,0.1],[500000,0.08],[1000000,0.06],[2500000,0.04],[5000000,0.02],[10000000,0]],
            fees = instrument.get('fees', [])
            fees_maker = instrument.get('fees_maker', [])

            if fees:
                market.taker_fee = round(fees[0][1] * 0.01, 6)
            if fees_maker:
                market.maker_fee = round(fees_maker[0][1] * 0.01, 6)

            if instrument.get('fee_volume_currency'):
                market.fee_currency = instrument['fee_volume_currency']

            if quote_asset != self.BASE_QUOTE:
                # from XXBTZUSD / XXBTZEUR ...
                # @todo
                pass
                # if self._tickers_data.get(quote_asset+self.BASE_QUOTE):
                #     market.base_exchange_rate = float(self._tickers_data.get(quote_asset+self.BASE_QUOTE, {'price', '1.0'})['price'])
                # elif self._tickers_data.get(self.BASE_QUOTE+quote_asset):
                #     market.base_exchange_rate = 1.0 / float(self._tickers_data.get(self.BASE_QUOTE+quote_asset, {'price', '1.0'})['price'])
                # else:
                #     market.base_exchange_rate = 1.0
            else:
                market.base_exchange_rate = 1.0

            # @todo contract_size
            # market.contract_size = 1.0 / mid_price
            # market.value_per_pip = market.contract_size / mid_price

            # volume 24h : not have here

            # notify for strategy
            self.service.notify(Signal.SIGNAL_MARKET_INFO_DATA, self.name,
                                (market_id, market))

            # store the last market info to be used for backtesting
            if not self._read_only:
                Database.inst().store_market_info((
                    self.name,
                    market.market_id,
                    market.symbol,
                    market.market_type,
                    market.unit_type,
                    market.contract_type,  # type
                    market.trade,
                    market.orders,  # type
                    market.base,
                    market.base_display,
                    market.base_precision,  # base
                    market.quote,
                    market.quote_display,
                    market.quote_precision,  # quote
                    market.expiry,
                    int(market.last_update_time * 1000.0),  # expiry, timestamp
                    str(market.lot_size),
                    str(market.contract_size),
                    str(market.base_exchange_rate),
                    str(market.value_per_pip),
                    str(market.one_pip_means),
                    '-',
                    *size_limits,
                    *notional_limits,
                    *price_limits,
                    str(market.maker_fee),
                    str(market.taker_fee),
                    str(market.maker_commission),
                    str(market.taker_commission)))

        return market

    def fetch_order_book(self, market_id):
        # https://api.kraken.com/0/public/Depth
        pass

    #
    # protected
    #

    def __prefetch_markets(self):
        # size limits from conf
        self._size_limits = self.service.watcher_config(self._name).get(
            "size-limits", {})

        self._assets = self._connector.assets()
        self._instruments = self._connector.instruments()

    def __on_depth_data(self, data):
        # @ref https://www.kraken.com/en-us/features/websocket-api#message-book
        pass

    def __on_ticker_data(self, data):
        if isinstance(data, list) and data[2] == "ticker":
            market_id = self._wsname_lookup.get(data[3])
            base_asset, quote_asset = data[3].split('/')

            if not market_id:
                return

            last_update_time = time.time()
            ticker = data[1]

            bid = float(ticker['b'][0])
            ofr = float(ticker['a'][0])

            vol24_base = float(ticker['v'][0])
            vol24_quote = float(ticker['v'][0]) * float(ticker['p'][0])

            # compute base_exchange_rate (its always over primary account currency)
            # @todo
            # if quote_asset != self.BASE_QUOTE:
            #     if quote_asset in self._assets:
            #         pass  # @todo direct or indirect
            #     else:
            #         market.base_exchange_rate = 1.0  # could be EURUSD... but we don't have
            # else:
            #     market.base_exchange_rate = 1.0

            if bid > 0.0 and ofr > 0.0:
                market_data = (market_id, last_update_time > 0,
                               last_update_time, bid, ofr, None, None, None,
                               vol24_base, vol24_quote)
                self.service.notify(Signal.SIGNAL_MARKET_DATA, self.name,
                                    market_data)

        elif isinstance(data, dict):
            if data['event'] == "subscriptionStatus" and data[
                    'channelName'] == "ticker":
                # @todo register channelID...
                # {'channelID': 93, 'channelName': 'trade', 'event': 'subscriptionStatus', 'pair': 'ETH/USD', 'status': 'subscribed', 'subscription': {'name': 'ticker'}}
                pass

    def __on_trade_data(self, data):
        if isinstance(data, list) and data[2] == "trade":
            market_id = self._wsname_lookup.get(data[3])

            if not market_id:
                return

            for trade in data[1]:
                bid = float(trade[0])
                ofr = float(trade[0])
                vol = float(trade[1])
                trade_time = float(trade[2])
                # side = trade[3]

                tick = (trade_time, bid, ofr, vol)

                # store for generation of OHLCs
                self.service.notify(Signal.SIGNAL_TICK_DATA, self.name,
                                    (market_id, tick))

                if not self._read_only and self._store_trade:
                    Database.inst().store_market_trade(
                        (self.name, market_id, int(trade_time * 1000.0),
                         trade[0], trade[0], trade[1]))

                for tf in Watcher.STORED_TIMEFRAMES:
                    # generate candle per timeframe
                    with self._mutex:
                        candle = self.update_ohlc(market_id, tf, trade_time,
                                                  bid, ofr, vol)

                    if candle is not None:
                        self.service.notify(Signal.SIGNAL_CANDLE_DATA,
                                            self.name, (market_id, candle))

        elif isinstance(data, dict):
            if data['event'] == "subscriptionStatus" and data[
                    'channelName'] == "trade":
                # @todo register channelID...
                # {'channelID': 93, 'channelName': 'trade', 'event': 'subscriptionStatus', 'pair': 'ETH/USD', 'status': 'subscribed', 'subscription': {'name': 'trade'}}
                pass

    def __on_kline_data(self, data):
        pass

    def __on_own_trades(self, data):
        pass

    def __on_open_orders(self, data):
        pass

    #
    # miscs
    #

    def price_history(self, market_id, timestamp):
        """
        Retrieve the historical price for a specific market id.
        """
        return None

    def update_markets_info(self):
        """
        Update market info.
        """
        self.__prefetch_markets()

        for market_id in self._watched_instruments:
            market = self.fetch_market(market_id)

            if market.is_open:
                market_data = (market_id, market.is_open,
                               market.last_update_time, market.bid, market.ofr,
                               market.base_exchange_rate, market.contract_size,
                               market.value_per_pip, market.vol24h_base,
                               market.vol24h_quote)
            else:
                market_data = (market_id, market.is_open,
                               market.last_update_time, None, None, None, None,
                               None, None, None)

            self.service.notify(Signal.SIGNAL_MARKET_DATA, self.name,
                                market_data)

    def fetch_candles(self,
                      market_id,
                      timeframe,
                      from_date=None,
                      to_date=None,
                      n_last=None):
        if timeframe not in self.TF_MAP:
            logger.error("Watcher %s does not support timeframe %s" %
                         (self.name, timeframe))
            return

        candles = []

        # second timeframe to kraken interval
        interval = self.TF_MAP[timeframe]

        try:
            candles = self._connector.get_historical_candles(
                market_id, interval, from_date, to_date)
        except Exception as e:
            logger.error("Watcher %s cannot retrieve candles %s on market %s" %
                         (self.name, interval, market_id))
            error_logger.error(traceback.format_exc())

        count = 0

        for candle in candles:
            count += 1
            # store (timestamp, open bid, high bid, low bid, close bid, open ofr, high ofr, low ofr, close ofr, volume)
            if candle[0] is not None and candle[1] is not None and candle[
                    2] is not None and candle[3] is not None:
                yield ((candle[0], candle[1], candle[2], candle[3], candle[4],
                        candle[1], candle[2], candle[3], candle[4], candle[5]))

        logger.info(
            "Watcher %s has retrieved on market %s %s candles for timeframe %s"
            % (self.name, market_id, count, interval))
Exemple #4
0
class KrakenWatcher(Watcher):
    """
    Kraken market watcher using REST + WS.

    @todo create order, cancel order, position info
    @todo contract_size, value_per_pip, base_exchange_rate (initials and updates)
    @todo fee from 30 day traded volume
    @todo order book WS
    """

    BASE_QUOTE = "ZUSD"

    TF_MAP = {
        60: 1,          # 1m
        300: 5,         # 5m
        900: 15,        # 15m
        1800: 30,       # 30m
        3600: 60,       # 1h
        14400: 240,     # 4h
        86400.0: 1440,  # 1d
        # 604800: 10080,  # 1w (not allowed because starts on thuesday)
        # 1296000: 21600  # 15d
    }

    def __init__(self, service):
        super().__init__("kraken.com", service, Watcher.WATCHER_PRICE_AND_VOLUME)

        self._connector = None
        self._depths = {}  # depth chart per symbol tuple (last_id, bids, ofrs)

        self._acount_data = {}
        self._symbols_data = {}
        self._tickers_data = {}

        self._last_trade_id = {}

        self.__configured_symbols = set()  # cache for configured symbols set
        self.__matching_symbols = set()    # cache for matching symbols
        self.__ws_symbols = set()          # cache for matching symbols WS names

        self._reconnect_user_ws = False

        self._assets = {}
        self._instruments = {}
        self._wsname_lookup = {}

        self._ws_own_trades = {'status': False, 'version': "0", 'ping': 0.0, 'subscribed': False}
        self._ws_open_orders = {'status': False, 'version': "0", 'ping': 0.0, 'subscribed': False}

        self._last_ws_hearbeat = 0.0

    def connect(self):
        super().connect()

        with self._mutex:
            try:
                self._ready = False
                self._connecting = True

                identity = self.service.identity(self._name)

                if identity:
                    if not self._connector:
                        self._connector = Connector(
                            self.service,
                            identity.get('account-id', ""),
                            identity.get('api-key'),
                            identity.get('api-secret'),
                            identity.get('host'))

                    if not self._connector.connected or not self._connector.ws_connected:
                        self._connector.connect()

                    if self._connector and self._connector.connected:
                        #
                        # assets and instruments
                        #

                        # get all products symbols
                        self._available_instruments = set()

                        # prefetch all markets data with a single request to avoid one per market
                        self.__prefetch_markets()

                        for asset_name, details in self._assets.items():
                            # {'aclass': 'currency', 'altname': 'ADA', 'decimals': 8, 'display_decimals': 6},
                            pass

                        instruments = self._instruments
                        configured_symbols = self.configured_symbols()
                        matching_symbols = self.matching_symbols_set(configured_symbols, [instrument for instrument in list(self._instruments.keys())])

                        # cache them
                        self.__configured_symbols = configured_symbols
                        self.__matching_symbols = matching_symbols

                        for market_id, instrument in instruments.items():
                            self._available_instruments.add(market_id)

                        #
                        # user data
                        #

                        ws_token = self._connector.get_ws_token()

                        if ws_token and ws_token.get('token'):
                            self._connector.ws.subscribe_private(
                                subscription={
                                    'name': 'ownTrades',
                                    'token': ws_token['token']
                                },
                                callback=self.__on_own_trades
                            )

                            self._connector.ws.subscribe_private(
                                subscription={
                                    'name': 'openOrders',
                                    'token': ws_token['token']
                                },
                                callback=self.__on_open_orders
                            )

                        # and start ws manager if necessarry
                        try:
                            self._connector.ws.start()
                        except RuntimeError:
                            logger.debug("%s WS already started..." % (self.name))

                        # once market are init
                        self._ready = True
                        self._connecting = False

                        logger.debug("%s connection successed" % (self.name))

            except Exception as e:
                error_logger.error(repr(e))
                traceback_logger.error(traceback.format_exc())

        if self._ready and self._connector and self._connector.connected:
            self.service.notify(Signal.SIGNAL_WATCHER_CONNECTED, self.name, time.time())

    def disconnect(self):
        super().disconnect()

        with self._mutex:
            try:
                if self._connector:
                    self._connector.disconnect()
                    self._connector = None

                self._ready = False
            except Exception as e:
                error_logger.error(repr(e))
                traceback_logger.error(traceback.format_exc())

    @property
    def connector(self):
        return self._connector

    @property
    def connected(self):
        return self._connector is not None and self._connector.connected and self._connector.ws_connected

    @property
    def authenticated(self):
        return self._connector and self._connector.authenticated

    #
    # instruments
    #

    def subscribe(self, market_id, timeframe, ohlc_depths=None, order_book_depth=None):
        result = False
        with self._mutex:
            try:
                if market_id not in self.__matching_symbols:
                    return False

                instrument = self._instruments.get(market_id)
                if not instrument:
                    return False

                pairs = []

                # live data
                pairs.append(instrument['wsname'])
                self._wsname_lookup[instrument['wsname']] = market_id

                # fetch from 1m to 1w
                if self._initial_fetch:
                    self.fetch_and_generate(market_id, Instrument.TF_1M, self.DEFAULT_PREFETCH_SIZE*3, Instrument.TF_3M)
                    self.fetch_and_generate(market_id, Instrument.TF_5M, self.DEFAULT_PREFETCH_SIZE, None)
                    self.fetch_and_generate(market_id, Instrument.TF_15M, self.DEFAULT_PREFETCH_SIZE*2, Instrument.TF_30M)
                    self.fetch_and_generate(market_id, Instrument.TF_1H, self.DEFAULT_PREFETCH_SIZE*2, Instrument.TF_2H)
                    self.fetch_and_generate(market_id, Instrument.TF_4H, self.DEFAULT_PREFETCH_SIZE, None)
                    self.fetch_and_generate(market_id, Instrument.TF_1D, self.DEFAULT_PREFETCH_SIZE*7, Instrument.TF_1W)

                    logger.info("%s prefetch for %s" % (self.name, market_id))

                # one more watched instrument
                self.insert_watched_instrument(market_id, [0])

                if pairs:
                    self._connector.ws.subscribe_public(
                        subscription={
                            'name': 'ticker'
                        },
                        pair=pairs,
                        callback=self.__on_ticker_data
                    )

                    self._connector.ws.subscribe_public(
                        subscription={
                            'name': 'trade'
                        },
                        pair=pairs,
                        callback=self.__on_trade_data
                    )

                    if order_book_depth and order_book_depth in (10, 25, 100, 500, 1000):
                        self._connector.ws.subscribe_public(
                            subscription={
                                'name': 'book'
                            },
                            pair=pairs,
                            depth=order_book_depth,
                            callback=self.__on_depth_data
                        )

                    result = True

            except Exception as e:
                error_logger.error(repr(e))

        return result

    def unsubscribe(self, market_id, timeframe):
        with self._mutex:
            if market_id in self._multiplex_handlers:
                self._multiplex_handlers[market_id].close()
                del self._multiplex_handlers[market_id]

                return True

        return False

    #
    # processing
    #

    def pre_update(self):
        if not self._ready or self._connector is None or not self._connector.connected or not self._connector.ws_connected:
            # retry in 2 second
            self._ready = False
            self._connector = None

            time.sleep(2)
            self.connect()
            return

    def update(self):
        if not super().update():
            return False

        if not self.connected:
            return False

        if 0:#self._reconnect_user_ws:
            ws_token = self._connector.get_ws_token()

            self._connector.ws.stop_socket('ownTrades')
            self._connector.ws.stop_socket('openOrders')

            if ws_token and ws_token.get('token'):
                self._connector.ws.subscribe_private(
                    subscription={
                        'name': 'ownTrades',
                        'token': ws_token['token']
                    },
                    callback=self.__on_own_trades
                )

                self._connector.ws.subscribe_private(
                    subscription={
                        'name': 'openOrders',
                        'token': ws_token['token']
                    },
                    callback=self.__on_open_orders
                )

            self._reconnect_user_ws = False

        #
        # ohlc close/open
        #

        with self._mutex:
            self.update_from_tick()

        #
        # market info update (each 4h)
        #

        if time.time() - self._last_market_update >= KrakenWatcher.UPDATE_MARKET_INFO_DELAY:  # only once per 4h
            try:
                self.update_markets_info()
                self._last_market_update = time.time()
            except Exception as e:
                error_logger.error("update_update_markets_info %s" % str(e))

        return True

    def post_update(self):
        super().post_update()
        time.sleep(0.0005)

    def post_run(self):
        super().post_run()

    def fetch_market(self, market_id):
        """
        Fetch and cache it. It rarely changes.
        """
        instrument = self._instruments.get(market_id)

        market = None

        if instrument:
            market = Market(market_id, instrument['altname'])

            market.is_open = True
            market.expiry = '-'

            # "wsname":"XBT\/USD"
            # wsname = WebSocket pair name (if available)
            # pair_decimals = scaling decimal places for pair
            # lot_decimals = scaling decimal places for volume
            # lot_multiplier = amount to multiply lot volume by to get currency volume

            # "aclass_base":"currency"
            base_asset = instrument['base']  # XXBT
            market.set_base(base_asset, base_asset, instrument['pair_decimals'])

            # "aclass_quote":"currency"
            quote_asset = instrument['quote']  # ZUSD 
            market.set_quote(quote_asset, quote_asset, instrument['lot_decimals'])  # 8

            # tick size at the base asset precision
            market.one_pip_means = math.pow(10.0, -instrument['pair_decimals'])  # 1
            market.value_per_pip = 1.0
            market.contract_size = 1.0
            market.lot_size = 1.0  # "lot":"unit", "lot_multiplier":1

            # "margin_call":80, "margin_stop":40
            # margin_call = margin call level
            # margin_stop = stop-out/liquidation margin level

            leverages = set(instrument.get('leverage_buy', []))
            leverages.intersection(set(instrument.get('leverage_sell', [])))

            market.margin_factor = 1.0 / max(leverages) if len(leverages) > 0 else 1.0

            market.set_leverages(leverages)

            size_limit = self._size_limits.get(instrument['altname'], {})
            min_size = size_limit.get('min-size', 1.0)

            size_limits = [str(min_size), "0.0", str(min_size)]
            notional_limits = ["0.0", "0.0", "0.0"]
            price_limits = ["0.0", "0.0", "0.0"]

            market.set_size_limits(float(size_limits[0]), float(size_limits[1]), float(size_limits[2]))
            market.set_price_limits(float(price_limits[0]), float(price_limits[1]), float(price_limits[2]))
            market.set_notional_limits(float(notional_limits[0]), 0.0, 0.0)

            # "lot":"unit"
            market.unit_type = Market.UNIT_AMOUNT
            market.market_type = Market.TYPE_CRYPTO
            market.contract_type = Market.CONTRACT_SPOT

            market.trade = Market.TRADE_ASSET
            if leverages:
                market.trade |= Market.TRADE_MARGIN
                market.trade |= Market.TRADE_FIFO

            # orders capacities
            market.orders = Order.ORDER_LIMIT | Order.ORDER_MARKET | Order.ORDER_STOP | Order.ORDER_TAKE_PROFIT

            # @todo take the first but it might depends of the traded volume per 30 days, then request volume window to got it
            # "fees":[[0,0.26],[50000,0.24],[100000,0.22],[250000,0.2],[500000,0.18],[1000000,0.16],[2500000,0.14],[5000000,0.12],[10000000,0.1]],
            # "fees_maker":[[0,0.16],[50000,0.14],[100000,0.12],[250000,0.1],[500000,0.08],[1000000,0.06],[2500000,0.04],[5000000,0.02],[10000000,0]],
            fees = instrument.get('fees', [])
            fees_maker = instrument.get('fees_maker', [])

            if fees:
                market.taker_fee = round(fees[0][1] * 0.01, 6)
            if fees_maker:
                market.maker_fee = round(fees_maker[0][1] * 0.01, 6)

            if instrument.get('fee_volume_currency'):
                market.fee_currency = instrument['fee_volume_currency']

            if quote_asset != self.BASE_QUOTE:
                # from XXBTZUSD / XXBTZEUR ...
                # @todo
                pass
                # if self._tickers_data.get(quote_asset+self.BASE_QUOTE):
                #     market.base_exchange_rate = float(self._tickers_data.get(quote_asset+self.BASE_QUOTE, {'price', '1.0'})['price'])
                # elif self._tickers_data.get(self.BASE_QUOTE+quote_asset):
                #     market.base_exchange_rate = 1.0 / float(self._tickers_data.get(self.BASE_QUOTE+quote_asset, {'price', '1.0'})['price'])
                # else:
                #     market.base_exchange_rate = 1.0
            else:
                market.base_exchange_rate = 1.0

            # @todo contract_size
            # market.contract_size = 1.0 / mid_price
            # market.value_per_pip = market.contract_size / mid_price

            # volume 24h : not have here

            # notify for strategy
            self.service.notify(Signal.SIGNAL_MARKET_INFO_DATA, self.name, (market_id, market))

            # store the last market info to be used for backtesting
            if not self._read_only:
                Database.inst().store_market_info((self.name, market.market_id, market.symbol,
                    market.market_type, market.unit_type, market.contract_type,  # type
                    market.trade, market.orders,  # type
                    market.base, market.base_display, market.base_precision,  # base
                    market.quote, market.quote_display, market.quote_precision,  # quote
                    market.expiry, int(market.last_update_time * 1000.0),  # expiry, timestamp
                    str(market.lot_size), str(market.contract_size), str(market.base_exchange_rate),
                    str(market.value_per_pip), str(market.one_pip_means), '-',
                    *size_limits,
                    *notional_limits,
                    *price_limits,
                    str(market.maker_fee), str(market.taker_fee), str(market.maker_commission), str(market.taker_commission))
                )

        return market

    def fetch_order_book(self, market_id):
        # https://api.kraken.com/0/public/Depth
        pass

    #
    # protected
    #

    def __prefetch_markets(self):
        # size limits from conf
        self._size_limits = self.service.watcher_config(self._name).get("size-limits", {})

        self._assets = self._connector.assets()
        self._instruments = self._connector.instruments()

    def __on_depth_data(self, data):
        # @ref https://www.kraken.com/en-us/features/websocket-api#message-book
        pass

    def __on_ticker_data(self, data):
        if isinstance(data, list) and data[2] == "ticker":
            market_id = self._wsname_lookup.get(data[3])
            base_asset, quote_asset = data[3].split('/')

            if not market_id:
                return

            last_update_time = time.time()
            ticker = data[1]
            
            bid = float(ticker['b'][0])
            ofr = float(ticker['a'][0])

            vol24_base = float(ticker['v'][0])
            vol24_quote = float(ticker['v'][0]) * float(ticker['p'][0])

            # compute base_exchange_rate (its always over primary account currency)
            # @todo
            # if quote_asset != self.BASE_QUOTE:
            #     if quote_asset in self._assets:
            #         pass  # @todo direct or indirect
            #     else:
            #         market.base_exchange_rate = 1.0  # could be EURUSD... but we don't have
            # else:
            #     market.base_exchange_rate = 1.0

            if bid > 0.0 and ofr > 0.0:
                market_data = (market_id, last_update_time > 0, last_update_time, bid, ofr, None, None, None, vol24_base, vol24_quote)
                self.service.notify(Signal.SIGNAL_MARKET_DATA, self.name, market_data)

        elif isinstance(data, dict):
            if data['event'] == "subscriptionStatus":
                if data['status'] == "subscribed" and data['channelName'] == "ticker":
                    # @todo register channelID...
                    # {'channelID': 93, 'channelName': 'trade', 'event': 'subscriptionStatus', 'pair': 'ETH/USD', 'status': 'subscribed', 'subscription': {'name': 'ticker'}}
                    pass

    def __on_trade_data(self, data):
        if isinstance(data, list) and data[2] == "trade":
            market_id = self._wsname_lookup.get(data[3])

            if not market_id:
                return

            for trade in data[1]:
                bid = float(trade[0])
                ofr = float(trade[0])
                vol = float(trade[1])
                trade_time = float(trade[2])
                # side = trade[3]

                tick = (trade_time, bid, ofr, vol)

                # store for generation of OHLCs
                self.service.notify(Signal.SIGNAL_TICK_DATA, self.name, (market_id, tick))

                if not self._read_only and self._store_trade:
                    Database.inst().store_market_trade((self.name, market_id, int(trade_time*1000.0), trade[0], trade[0], trade[1]))

                for tf in Watcher.STORED_TIMEFRAMES:
                    # generate candle per timeframe
                    with self._mutex:
                        candle = self.update_ohlc(market_id, tf, trade_time, bid, ofr, vol)

                    if candle is not None:
                        self.service.notify(Signal.SIGNAL_CANDLE_DATA, self.name, (market_id, candle))

        elif isinstance(data, dict):
            if data['event'] == "subscriptionStatus":
                if data['status'] == "subscribed" and data['channelName'] == "trade":
                    # @todo register channelID...
                    # {'channelID': 93, 'channelName': 'trade', 'event': 'subscriptionStatus', 'pair': 'ETH/USD', 'status': 'subscribed', 'subscription': {'name': 'trade'}}
                    pass

    def __on_kline_data(self, data):
        pass

    def __on_own_trades(self, data):
        if isinstance(data, list) and data[1] == "ownTrades":
            # [{'xxxx-yyyy-zzzz': {'cost': '110.15956', 'fee': '0.28642', 'margin': '22.03191', 'ordertxid': 'xxxx-yyyy-zzzz', 'ordertype': 'market', 'pair': 'XBT/EUR', 'posstatus': 'Closing',
            #    'postxid': 'xxxx-yyyy-zzzz', 'price': '7343.97067', 'time': '1571773989.359761', 'type': 'buy', 'vol': '0.01500000'}},
            #   {'xxxx-yyyy-zzzz': {'cost': '110.58150', 'fee': '0.29857', 'margin': '22.11630', 'ordertxid': 'xxxx-yyyy-zzzz', 'ordertype': 'market', 'pair': 'XBT/EUR', 'posstatus': 'Opened',
            #    'postxid': 'xxxx-yyyy-zzzz', 'price': '7372.10000', 'time': '1571725122.392658', 'type': 'sell', 'vol': '0.01500000'}}]
            for entry in data[0]:
                txid = list(entry.keys())[0]
                trade = list(entry.values())[0]

                exec_logger.info("kraken.com ownTrades : %s - %s" % (txid, trade))

                market_id = self._wsname_lookup.get(trade['pair'])
                # base_asset, quote_asset = trade['pair'].split('/')

                if not market_id:
                    continue

                timestamp = float(trade['time'])

                order_id = trade['ordertxid']
                position_id = trade['postxid']

                side = Order.LONG if trade['type'] == "buy" else Order.SHORT
                # cost, fee, vol, margin, orderType

                posstatus = trade.get('posstatus', "")

                if posstatus == 'Closing':
                    pass
                    #     client_order_id = str(data['c'])
                    #     reason = ""

                    #     if data['r'] == 'INSUFFICIENT_BALANCE':
                    #         reason = 'insufficient balance'

                    #     self.service.notify(Signal.SIGNAL_ORDER_REJECTED, self.name, (symbol, client_order_id))

                    # elif (data['x'] == 'TRADE') and (data['X'] == 'FILLED' or data['X'] == 'PARTIALLY_FILLED'):
                    #     order_id = str(data['i'])
                    #     client_order_id = str(data['c'])

                    #     timestamp = float(data['T']) * 0.001  # transaction time

                    #     price = None
                    #     stop_price = None

                    #     if data['o'] == 'LIMIT':
                    #         order_type = Order.ORDER_LIMIT
                    #         price = float(data['p'])

                    #     elif data['o'] == 'MARKET':
                    #         order_type = Order.ORDER_MARKET

                    #     elif data['o'] == 'STOP_LOSS':
                    #         order_type = Order.ORDER_STOP
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'STOP_LOSS_LIMIT':
                    #         order_type = Order.ORDER_STOP_LIMIT
                    #         price = float(data['p'])
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'TAKE_PROFIT':
                    #         order_type = Order.ORDER_TAKE_PROFIT
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'TAKE_PROFIT_LIMIT':
                    #         order_type = Order.ORDER_TAKE_PROFIT_LIMIT
                    #         price = float(data['p'])
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'LIMIT_MAKER':
                    #         order_type = Order.ORDER_LIMIT
                    #         price = float(data['p'])

                    #     else:
                    #         order_type = Order.ORDER_LIMIT

                    #     if data['f'] == 'GTC':
                    #         time_in_force = Order.TIME_IN_FORCE_GTC
                    #     elif data['f'] == 'IOC':
                    #         time_in_force = Order.TIME_IN_FORCE_IOC
                    #     elif data['f'] == 'FOK':
                    #         time_in_force = Order.TIME_IN_FORCE_FOK
                    #     else:
                    #         time_in_force = Order.TIME_IN_FORCE_GTC

                    #     order = {
                    #         'id': order_id,
                    #         'symbol': symbol,
                    #         'type': order_type,
                    #         'trade-id': str(data['t']),
                    #         'direction': Order.LONG if data['S'] == 'BUY' else Order.SHORT,
                    #         'timestamp': timestamp,
                    #         'quantity': float(data['q']),
                    #         'price': price,
                    #         'stop-price': stop_price,
                    #         'exec-price': float(data['L']),
                    #         'filled': float(data['l']),
                    #         'cumulative-filled': float(data['z']),
                    #         'quote-transacted': float(data['Y']),  # similar as float(data['Z']) for cumulative
                    #         'stop-loss': None,
                    #         'take-profit': None,
                    #         'time-in-force': time_in_force,
                    #         'commission-amount': float(data['n']),
                    #         'commission-asset': data['N'],
                    #         'maker': data['m'],   # trade execution over or counter the market : true if maker, false if taker
                    #         'fully-filled': data['X'] == 'FILLED'  # fully filled status else its partially
                    #     }

                    #     self.service.notify(Signal.SIGNAL_ORDER_TRADED, self.name, (symbol, order, client_order_id))

                elif posstatus == 'Opened':
                    pass
                    #     order_id = str(data['i'])
                    #     timestamp = float(data['O']) * 0.001  # order creation time
                    #     client_order_id = str(data['c'])

                    #     iceberg_qty = float(data['F'])

                    #     price = None
                    #     stop_price = None

                    #     if data['o'] == 'LIMIT':
                    #         order_type = Order.ORDER_LIMIT
                    #         price = float(data['p'])

                    #     elif data['o'] == 'MARKET':
                    #         order_type = Order.ORDER_MARKET

                    #     elif data['o'] == 'STOP_LOSS':
                    #         order_type = Order.ORDER_STOP
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'STOP_LOSS_LIMIT':
                    #         order_type = Order.ORDER_STOP_LIMIT
                    #         price = float(data['p'])
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'TAKE_PROFIT':
                    #         order_type = Order.ORDER_TAKE_PROFIT
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'TAKE_PROFIT_LIMIT':
                    #         order_type = Order.ORDER_TAKE_PROFIT_LIMIT
                    #         price = float(data['p'])
                    #         stop_price = float(data['P'])

                    #     elif data['o'] == 'LIMIT_MAKER':
                    #         order_type = Order.ORDER_LIMIT
                    #         price = float(data['p'])

                    #     else:
                    #         order_type = Order.ORDER_LIMIT

                    #     if data['f'] == 'GTC':
                    #         time_in_force = Order.TIME_IN_FORCE_GTC
                    #     elif data['f'] == 'IOC':
                    #         time_in_force = Order.TIME_IN_FORCE_IOC
                    #     elif data['f'] == 'FOK':
                    #         time_in_force = Order.TIME_IN_FORCE_FOK
                    #     else:
                    #         time_in_force = Order.TIME_IN_FORCE_GTC

                    #     order = {
                    #         'id': order_id,
                    #         'symbol': symbol,
                    #         'direction': Order.LONG if data['S'] == 'BUY' else Order.SHORT,
                    #         'type': order_type,
                    #         'timestamp': event_timestamp,
                    #         'quantity': float(data['q']),
                    #         'price': price,
                    #         'stop-price': stop_price,
                    #         'time-in-force': time_in_force,
                    #         'stop-loss': None,
                    #         'take-profit': None
                    #     }

                    #     self.service.notify(Signal.SIGNAL_ORDER_OPENED, self.name, (symbol, order, client_order_id))

                else:
                    pass

        elif isinstance(data, dict):
            if data['event'] == 'heartBeat':
                self._ws_own_trades['ping'] = time.time()

            elif data['event'] == 'systemStatus':
                # {'connectionID': 000, 'event': 'systemStatus', 'status': 'online', 'version': '0.2.0'}
                self._ws_own_trades['status'] = data['status'] == "online"
                self._ws_own_trades['version'] = data['version']

            elif data['event'] == "subscriptionStatus":
                if data['status'] == "error":
                    error_logger.error("%s - %s" % (data['errorMessage'], data['name']))
                    self._ws_own_trades['status'] = False
                    self._reconnect_user_ws = True

                elif data['status'] == "subscribed" and data['channelName'] == "ownTrades":
                    self._ws_own_trades['subscribed'] = True

    def __on_open_orders(self, data):
        if isinstance(data, list) and data[1] == "openOrders":
            # [{'xxxx-yyyy-zzzz': {'avg_price': '0.00000', 'cost': '0.00000', 'descr': {'close': None, 'leverage': None, 'order': 'sell 9.99355396 LTC/EUR @ limit 56.15000',
            #   'ordertype': 'limit', 'pair': 'LTC/EUR', 'price': '56.15000', 'price2': '0.00000', 'type': 'sell'}, 'expiretm': None, 'fee': '0.00000', 'limitprice': '0.00000',
            #   'misc': '', 'oflags': 'fciq', 'opentm': '1573672059.209149', 'refid': None, 'starttm': None, 'status': 'open', 'stopprice': '0.00000', 'userref': 0,
            #   'vol': '9.99355396', 'vol_exec': '0.00000000'}}
            for entry in data[0]:
                order_id = list(entry.keys())[0]
                order = list(entry.values())[0]

                exec_logger.info("kraken.com openOrders : %s - %s" % (order_id, order))

                status = order.get("status", "")

                if status == "open":
                    ref_order_id = order.get('refid')
                    pass

                elif status == "closed":
                    pass

                elif status == "updated":
                    pass

                elif status == "deleted":
                    pass

        elif isinstance(data, dict):
            if data['event'] == 'heartBeat':
                self._ws_open_orders['ping'] = time.time()

            elif data['event'] == 'systemStatus':
                # {'connectionID': 000, 'event': 'systemStatus', 'status': 'online', 'version': '0.2.0'}
                self._ws_open_orders['status'] = data['status'] == "online"
                self._ws_open_orders['version'] = data['version']

            elif data['event'] == "subscriptionStatus":
                if data['status'] == "error":
                    error_logger.error("%s - %s" % (data['errorMessage'], data['name']))
                    self._ws_open_orders['status'] = False
                    self._reconnect_user_ws = True

                elif data['status'] == "subscribed" and data['channelName'] == "openOrders":
                    self._ws_open_orders['subscribed'] = True

    #
    # miscs
    #

    def price_history(self, market_id, timestamp):
        """
        Retrieve the historical price for a specific market id.
        """
        return None

    def update_markets_info(self):
        """
        Update market info.
        """
        self.__prefetch_markets()

        for market_id in self._watched_instruments:
            market = self.fetch_market(market_id)

            if market.is_open:
                market_data = (market_id, market.is_open, market.last_update_time, market.bid, market.ofr,
                        market.base_exchange_rate, market.contract_size, market.value_per_pip,
                        market.vol24h_base, market.vol24h_quote)
            else:
                market_data = (market_id, market.is_open, market.last_update_time, None, None, None, None, None, None, None)

            self.service.notify(Signal.SIGNAL_MARKET_DATA, self.name, market_data)

    def fetch_candles(self, market_id, timeframe, from_date=None, to_date=None, n_last=None):
        if timeframe not in self.TF_MAP:
            logger.error("Watcher %s does not support timeframe %s" % (self.name, timeframe))
            return

        candles = []

        # second timeframe to kraken interval
        interval = self.TF_MAP[timeframe]

        try:
            candles = self._connector.get_historical_candles(market_id, interval, from_date, to_date)
        except Exception as e:
            logger.error("Watcher %s cannot retrieve candles %s on market %s" % (self.name, interval, market_id))
            error_logger.error(traceback.format_exc())

        count = 0
        
        for candle in candles:
            count += 1
            # store (timestamp, open bid, high bid, low bid, close bid, open ofr, high ofr, low ofr, close ofr, volume)
            if candle[0] is not None and candle[1] is not None and candle[2] is not None and candle[3] is not None:
                yield((candle[0], candle[1], candle[2], candle[3], candle[4], candle[1], candle[2], candle[3], candle[4], candle[5]))

        logger.info("Watcher %s has retrieved on market %s %s candles for timeframe %s" % (self.name, market_id, count, interval))