From 365d5f245d7f5bebcf2c79dd4b46ece18e30741d Mon Sep 17 00:00:00 2001 From: aydin Date: Tue, 25 Jan 2022 16:26:49 -0500 Subject: [PATCH 01/13] partial kraken integration --- blankly/__init__.py | 1 + blankly/exchanges/auth/auth_factory.py | 3 + blankly/exchanges/exchange.py | 1 - .../interfaces/direct_calls_factory.py | 6 + .../exchanges/interfaces/kraken/__init__.py | 0 blankly/exchanges/interfaces/kraken/kraken.py | 29 + .../exchanges/interfaces/kraken/kraken_api.py | 760 ++++++++++++++++++ .../interfaces/kraken/kraken_auth.py | 8 + .../interfaces/kraken/kraken_interface.py | 349 ++++++++ .../kraken/kraken_interface_utils.py | 37 + .../interfaces/kraken/kraken_websocket.py | 199 +++++ .../kraken/kraken_websocket_utils.py | 0 setup.py | 1 + .../kraken/test_kraken_interface.py | 149 ++++ 14 files changed, 1542 insertions(+), 1 deletion(-) create mode 100644 blankly/exchanges/interfaces/kraken/__init__.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken_api.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken_auth.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken_interface.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken_interface_utils.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken_websocket.py create mode 100644 blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py create mode 100644 tests/exchanges/interfaces/kraken/test_kraken_interface.py diff --git a/blankly/__init__.py b/blankly/__init__.py index b081f11b..e417e765 100644 --- a/blankly/__init__.py +++ b/blankly/__init__.py @@ -21,6 +21,7 @@ from blankly.exchanges.interfaces.alpaca.alpaca import Alpaca from blankly.exchanges.interfaces.oanda.oanda import Oanda from blankly.exchanges.interfaces.ftx.ftx import FTX +from blankly.exchanges.interfaces.kraken.kraken import Kraken from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade from blankly.frameworks.strategy import Strategy as Strategy diff --git a/blankly/exchanges/auth/auth_factory.py b/blankly/exchanges/auth/auth_factory.py index caf29ff2..eff8ca31 100644 --- a/blankly/exchanges/auth/auth_factory.py +++ b/blankly/exchanges/auth/auth_factory.py @@ -21,6 +21,7 @@ from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_auth import CoinbaseProAuth from blankly.exchanges.interfaces.oanda.oanda_auth import OandaAuth from blankly.exchanges.interfaces.ftx.ftx_auth import FTXAuth +from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth class AuthFactory: @@ -36,6 +37,8 @@ def create_auth(keys_file, exchange_name, portfolio_name): return OandaAuth(keys_file, portfolio_name) elif exchange_name == 'ftx': return FTXAuth(keys_file, portfolio_name) + elif exchange_name == 'kraken': + return KrakenAuth(keys_file, portfolio_name) elif exchange_name == 'paper_trade': return None else: diff --git a/blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py index c8923f75..678ee091 100644 --- a/blankly/exchanges/exchange.py +++ b/blankly/exchanges/exchange.py @@ -23,7 +23,6 @@ from blankly.exchanges.auth.auth_constructor import write_auth_cache from blankly.exchanges.auth.auth_factory import AuthFactory from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface -from blankly.exchanges.interfaces.ftx.ftx_interface import FTXInterface from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface diff --git a/blankly/exchanges/interfaces/direct_calls_factory.py b/blankly/exchanges/interfaces/direct_calls_factory.py index 3926dc93..8b124b73 100644 --- a/blankly/exchanges/interfaces/direct_calls_factory.py +++ b/blankly/exchanges/interfaces/direct_calls_factory.py @@ -24,6 +24,8 @@ from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI +from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI +from blankly.exchanges.interfaces.kraken.kraken_interface import KrakenInterface from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI from blankly.exchanges.interfaces.ftx.ftx_interface import FTXInterface @@ -67,5 +69,9 @@ def create(exchange_name: str, auth: ABCAuth, preferences_path: str = None): calls = OandaAPI(auth, preferences["settings"]["use_sandbox"]) return calls, OandaInterface(calls, preferences_path) + elif exchange_name == 'kraken': + calls = KrakenAPI(auth) + return calls, KrakenInterface(calls, preferences_path) + elif exchange_name == 'paper_trade': return None, None diff --git a/blankly/exchanges/interfaces/kraken/__init__.py b/blankly/exchanges/interfaces/kraken/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/blankly/exchanges/interfaces/kraken/kraken.py b/blankly/exchanges/interfaces/kraken/kraken.py new file mode 100644 index 00000000..40457c45 --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/kraken.py @@ -0,0 +1,29 @@ +from blankly.exchanges.exchange import Exchange +from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI + +class Kraken(Exchange): + def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None): + # Giving the preferences path as none allows us to create a default + Exchange.__init__(self, "kraken", portfolio_name, keys_path, settings_path) + + """ + Builds information about the asset on this exchange by making particular API calls + """ + + def get_asset_state(self, symbol): + """ + This determines the internal properties of the exchange block. + Should be implemented per-class because it requires different types of interaction with each exchange. + """ + # TODO Populate this with useful information + return self.interface.get_account(symbol) + + def get_exchange_state(self): + """ + Exchange state is the external properties for the exchange block + """ + # TODO Populate this with useful information + return self.interface.get_fees() + + def get_direct_calls(self) -> KrakenAPI: + return self.calls \ No newline at end of file diff --git a/blankly/exchanges/interfaces/kraken/kraken_api.py b/blankly/exchanges/interfaces/kraken/kraken_api.py new file mode 100644 index 00000000..a18cb9dc --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/kraken_api.py @@ -0,0 +1,760 @@ +import hmac +import hashlib +from multiprocessing.sharedctypes import Value +import time +import urllib +import base64 +import binascii +import requests +#from . import version +import requests +from blankly.exchanges.auth.abc_auth import ABCAuth +from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth + + +class API: + + def __init__(self, auth: ABCAuth, API_URL='https://api.kraken.com', timeout = 30): + self.__api_url = API_URL + self.session = requests.Session() + + self._api_key = auth.keys['API_SECRET'] + self._api_secret = auth.keys['API_KEY'] + self.timeout = [timeout] + self.proxy = {} + self.version = "0" + + + + # Short info about API client + self.headers = { + "User-Agent": "Blankly-finance", + "Version": self.version + } + + # List of all supported Request Strings. + self.supported_requests = { + "private": [ + "Balance", "TradeBalance", "OpenOrders", "ClosedOrders", "QueryOrders", "TradesHistory", + "QueryTrades", "OpenPositions", "Ledgers", "QueryLedgers", "TradeVolume", "AddOrder", + "CancelOrder", "DepositMethods", "DepositAddress", "DepositStatus", "WithdrawInfo", "Withdraw", + "WithdrawStatus", "WithdrawCancel" + ], + "public": [ + "Time", "Assets", "AssetPairs", "Ticker", "OHLC", "Depth", "Trades", "Spread" + ] + } + + def public(self, request, parameters=None): + """ + Creates a public API call. It does not require public and private keys to be set. + :param str request: One of ``Time``, ``Assets``, ``AssetPairs``, ``Ticker``, ``OHLC``, ``Depth``, \ + ``Trades`` and ``Spread``. + :param dict parameters: Additional parameters for each request. See Kraken documentation for more information. + :returns: JSON object representing Kraken API response. + :raises: ``ValueError``, if request string is not valid. ``ConnectionError`` and \ + ``ConnectionTimeout`` on connection problem. + + """ + + parameters = parameters if parameters else {} + + # Raise error on unsupported input + if request not in self.supported_requests["public"]: + raise ValueError("Request string % is not supported by public API calls.".format(request)) + + # Path to the requested api call + path = self.__api_url + '/' + self.version + '/public/' + request + + # Connect to API server + try: + response = requests.post( + path, + data=parameters, + headers=self.headers, + timeout=self.timeout[0], + proxies=self.proxy + ) + except requests.exceptions.ConnectionError: + raise ConnectionError() + except requests.exceptions.Timeout: + raise ConnectionTimeout() + except requests.exceptions.TooManyRedirects: + raise ConnectionError() + + if response.status_code != 200: + raise ConnectionError() + else: + + err_len = response.json()["error"] + if len(err_len) > 0: + raise ValueError(f"kraken API error: {err_len}") + else: + return response.json()["result"] + + def time(self)-> dict: + """ + { + "error":[ + + ], + "result":{ + "rfc1123":"Wed, 10 May 17 10:32:24 +0000", + "unixtime":1494412344 + } + } + + """ + + return self.public("Time") + + def assets(self, assets="") -> dict: + """ + { + "result":{ + "XMLN":{ + "altname":"MLN", + "decimals":10, + "display_decimals":5, + "aclass":"currency" + }, + + ... + } + } + """ + + # According to documentation, info and aclass have only those values: + parameters = { + "info": "info", + "aclass": "currency" + } + + # Asset is not required + if len(assets) != 0: + parameters["asset"] = assets + + return self.public("Assets", parameters) + + def asset_pairs(self, info="info", pairs=""): + """ + { + "error":[ + + ], + "result":{ + "XXRPXXBT":{ + "pair_decimals":8, + "lot":"unit", + "margin_call":80, + "fees_maker":[ + [ + 0, + 0.16 + ], + ... + ], + "altname":"XRPXBT", + "quote":"XXBT", + "fees":[ + [ + 0, + 0.26 + ], + ... + ], + "aclass_quote":"currency", + "margin_stop":40, + "base":"XXRP", + "lot_multiplier":1, + "fee_volume_currency":"ZUSD", + "aclass_base":"currency", + "leverage_sell":[ + + ], + "leverage_buy":[ + + ], + "lot_decimals":8 + }, + ... + } + } + + """ + if info not in ["info", "leverage", "fees", "margin"]: + raise ValueError("Value % is not a valid info string.".format(info)) + + parameters = { + "info": info, + } + + # Pair is not required and should be omnited if empty + if len(pairs) != 0: + parameters["pair"] = pairs + + return self.public("AssetPairs", parameters) + + def ticker(self, pairs): + """ + { + "error":[ + + ], + "result":{ + "XZECZEUR":{ + "t":[ + 789, + 1563 + ], + "h":[ + "93.88889", + "93.88889" + ], + "l":[ + "83.30000", + "82.90067" + ], + "a":[ + "91.13167", + "1", + "1.000" + ], + "b":[ + "91.13166", + "1", + "1.000" + ], + "v":[ + "1409.83478992", + "3185.73131989" + ], + "p":[ + "90.08671", + "87.74001" + ], + "c":[ + "91.13068", + "3.23282073" + ], + "o":"85.87561" + } + } + } + """ + parameters = { + "pair": pairs + } + + if len(pairs) == 0: + raise ValueError("Ticker pairs parameter is required.") + + return self.public("Ticker", parameters) + + def ohlc(self, pairs, interval=1, since=0) -> dict: + """ + { + "error":[ + + ], + "result":{ + "last":1494422340, + "XZECZEUR":[ + [ + 1494379260, + "86.93479", + "86.93479", + "86.93479", + "86.93479", + "0.00000", + "0.00000000", + 0 + ], + ... + ] + } + } + """ + if interval not in [1, 5, 15, 30, 60, 240, 1440, 10080, 21600]: + raise ValueError("Interval parameter value '%' is not valid.".format(interval)) + + parameters = { + "pair": pairs, + "interval": interval + } + + if since != 0: + parameters["since"] = since + + return self.public("OHLC", parameters) + + def depth(self, pairs, count=0): + """ + { + "error":[ + + ], + "result":{ + "XZECZEUR":{ + "asks":[ + [ + "89.92088", + "34.910", + 1494422730 + ], + ... + ], + "bids":[ + [ + "88.42616", + "4.755", + 1494422729 + ], + ... + ] + } + } + } + """ + parameters = { + "pair": pairs + } + + if count != 0: + parameters["count"] = count + + return self.public("Depth", parameters) + + def trades(self, pairs, since=0): + """ + { + "error":[ + + ], + "result":{ + "last":"1494423192560021193", + "XZECZEUR":[ + [ + "86.20035", + "0.44929000", + 1494369147.0533, + "b", + "l", + "" + ], + ... + ] + } + } + """ + parameters = { + "pair": pairs + } + + if since != 0: + parameters["since"] = since + + return self.public("Trades", parameters) + + def spread(self, pairs, since=0): + """ + { + "error":[ + + ], + "result":{ + "XZECZEUR":[ + [ + 1494423011, + "88.42539", + "88.91550" + ], + ... + ], + "last":1494423011 + } + } + """ + parameters = { + "pair": pairs + } + + if since != 0: + parameters["since"] = since + + return self.public("Spread", parameters) + + def private(self, request, parameters=None): + """ + Perform a private API call. + :warning: Calling this function requires public and private keys to be set in class constructor. + :param request: One of ``Balance``, ``TradeBalance``, ``OpenOrders``, ``ClosedOrders``, ``QueryOrders``, \ + ``TradesHistory``, ``QueryTrades``, ``OpenPositions``, ``Ledgers``, ``QueryLedgers``, \ + ``TradeVolume``, ``AddOrder``, ``CancelOrder``. + :param parameters: Additional parameters for each request. See Kraken documentation for more information. + :returns: A JSON object representing Kraken API response. + :raises: ValueError is request string is not supported. ConnectionError and ConnectionTimeout on \ + connection problem. + """ + + parameters = parameters if parameters else {} + + # Check if request string is supported by this version of API + if request not in self.supported_requests["private"]: + raise ValueError("Request string '%' is not supported by private API calls".format(request)) + + path = '/' + self.version + '/private/' + request + + # Nonce parameter is required. Each call should contain nonce with greater value than the previous one. + parameters["nonce"] = int(time.time() * 1000) + + # Parse parameters to URL query string + post = urllib.parse.urlencode(parameters) + + # UTF-8 string have to be encoded first + encoded = (str(parameters['nonce']) + post).encode() + + # Message consist of path and hash of POST parameters + message = path.encode() + hashlib.sha256(encoded).digest() + + + try: + signature = hmac.new(base64.b64decode(self._api_key), message, hashlib.sha512) + except binascii.Error: + raise ValueError("Private key is not a valid base64 encoded string") + + sigdigest = base64.b64encode(signature.digest()) + + # Requred headers + headers = { + 'API-Key': self._api_secret, + 'API-Sign': sigdigest.decode() + } + headers.update(self.headers) + + # Connect to API server + try: + response = requests.post(self.__api_url + path, data=parameters, headers=headers, timeout=self.timeout[0], + proxies=self.proxy) + except requests.exceptions.ConnectionError as e: + raise ConnectionError(e.response) + except requests.exceptions.Timeout: + raise ConnectionTimeout() + except requests.exceptions.TooManyRedirects: + raise ConnectionError() + + if response.status_code != 200: + raise ConnectionError() + else: + err_len = response.json()["error"] + if len(err_len) > 0: + raise ValueError(f"kraken API error: {err_len}") + else: + return response.json()["result"] + + def balance(self): + return self.private("Balance") + + def trade_balance(self, asset="ZUSD"): + parameters = { + "aclass": "currency", + "asset": asset + } + + return self.private("TradeBalance", parameters) + + def open_orders(self, trades=False, userref=""): + """ + Gets User opened orders. See `Kraken documentation `__. + :param trades: Include trades in output? True/False. + :param userref: Restrict results to given user reference ID. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + parameters = { + "trades": "true" if trades else "false", + } + + if len(userref) != 0: + parameters["userref"] = userref + + return self.private("OpenOrders", parameters) + + def closed_orders(self, trades=False, userref="", start="", end="", offset="", closetime="both"): + """ + Returns closed orders according to parameters. See `Kraken documentation `__. + :param trades: Include trades in response? + :param userref: Restrict response to given user reference ID. + :param start: Starting timestamp or order ID. + :param end: End timestamp or order ID. + :param offset: Result offset. + :param closetime: Which time to use. One of ``open``, ``close``, ``both``. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + parameters = { + "trades": "true" if trades else "false" + } + + if len(userref) != 0: + parameters["userref"] = userref + + if len(start) != 0: + parameters["start"] = start + + if len(end) != 0: + parameters["end"] = end + + if len(offset) != 0: + parameters["offset"] = offset + + if closetime not in ["open", "close", "both"]: + raise ValueError("Parameter closetime is not valid.") + + parameters["closetime"] = closetime + + return self.private("CloseOrders", parameters) + + def query_orders(self, trades=False, userref="", txid=""): + """ + Returns orders info. See `Kraken documentation `__. + :param trades: Include trades in output? + :param userref: User reference ID. + :param txid: Comma separated list of transaction IDs. Max 2O. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + parameters = { + "trades": "true" if trades else "false" + } + + if len(userref) != 0: + parameters["userref"] = userref + + if len(txid) != 0: + if len(txid.split(",")) > 20: + raise ValueError("Parameter txid can not contain more than 20 comma separated values.") + parameters["txid"] = txid + + return self.private("QueryOrders", parameters) + + def trades_history(self, ttype="all", trades=False, start="", end="", offset=""): + """ + Gets trades history. See `Kraken documentation `__. + :param ttype: Trade type. Method checks right input - it can be one of ``all``, ``any``, ``closed``, ``no``. + :param trades: Include trades related to position in result? + :param start: Unix timestamp or trade ID. + :param end: Unix timestamp of trade ID. + :param offset: History offset. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + if type not in ["all", "any", "closed", "closing", "no"]: + raise ValueError("Type parameter is not valid.") + + parameters = { + "type": ttype, + "trades": "true" if trades else "false" + } + + if len(start) != 0: + parameters["start"] = start + + if len(end) != 0: + parameters["end"] = end + + if len(offset) != 0: + parameters["ofs"] = offset + + return self.private("TradesHistory", parameters) + + def query_trades(self, trades=False, txid=""): + """ + Returns trade info. See `Kraken documentation `__. + :param trades: Include trades related to position in response. + :param txid: Comma separated list of transaction IDs. Max 2O. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + parameters = { + "trades": "true" if trades else "false" + } + + if len(txid) != 0: + if len(txid.split(",")) > 20: + raise ValueError("Parameter txid can not contain more than 20 comma separated values.") + parameters["txid"] = txid + + return self.private("QueryTrades", parameters) + + def open_positions(self, docalcs=False, txid=""): + """ + Returns open positions list. See `Kraken documentation `__. + :param txid: Comma delimited list of transaction IDs to restrict output to + :param docalcs: Include profit/loss calculations. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + parameters = { + "docals": "true" if docalcs else "false" + } + + if len(txid) != 0: + if len(txid.split(",")) > 20: + raise ValueError("Parameter txid can not contain more than 20 comma separated values.") + parameters["txid"] = txid + + return self.private("OpenPositions", parameters) + + def ledgers(self, asset="", ltype="all", start=0, end=0, offset=0): + """ + Returns ledgers list. See `Kraken documentation `__. + :param str asset: Comma delimited list of assets to restrict output to. + :param str ltype: Type of ledger to retrieve. One of ``all``, ``deposit``, ``withdrawal`` , \ + ``trade`` and ``margin``. + :param int or str start: Starting unix timestamp or ledger ID of results. + :param int or str end: Ending unix timestamp or ledger ID of results. + :param int offset: Result offset. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + if ltype not in ["all", "deposit", "withdrawal", "trade", "margin"]: + raise ValueError("Parameter type is not valid") + + parameters = { + "type": ltype, + "aclass": "currency" + } + + if len(asset) != 0: + parameters["asset"] = asset + + if start != 0: + parameters["start"] = start + + if end != 0: + parameters["end"] = end + + if offset != 0: + parameters["offset"] = offset + + return self.private("Ledgers", parameters) + + def query_ledgers(self, lid): + """ + Returns ledger info. See `Kraken documentation `__. + :param lid: Comma delimited list of ledger ids to query info about. Max 2O. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + + parameters = {} + + if len(lid) != 0: + if len(lid.split(",")) > 20: + raise ValueError("Parameter id can not contain more than 20 comma separated values.") + parameters["id"] = lid + + return self.private("QueryLedgers", parameters) + + def trade_volume(self, pair="", fee_info=False): + """ + Gets trade volume. See `Kraken documentation `__. + :param pair: Comma delimited list of asset pairs to get fee info on. + :param fee_info: Include fee info in results? + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + parameters = {} + + if len(pair) != 0: + if len(pair.split(",")) > 20: + raise ValueError("Parameter pair can not contain more than 20 comma separated values.") + parameters["pair"] = pair + + if fee_info: + parameters["fee-info"] = "true" + + self.private("TradeVoluem", parameters) + + def add_order(self, pair, otype, ordertype, price, volume, price2=-1, leverage="none", + oflags="", starttm=0, expiretm=0, userref="", validate=False): + """ + Adds exchange order to your account. See `Kraken documentation `__. + :param pair: Asset pair. + :param otype: Type of order (buy/sell). + :param ordertype: Order type. Method tests right input to this parameter and thus may raise ``ValueError``. + Only following values are allowed - ``market``, ``stop-loss``, ``take-profit``, ``stop-loss-profit``, + ``stop-loss-profit-limit``, ``stop-loss-limit``, ``take-profit-limit``, ``trailing-stop``, ``trailing-stop-limit`` + ``stop-loss-and-limit`` and ``settle-position``. + :param price: Price, meaning depends on order type. + :param volume: Order volume in lots. + :param price2: Price, meaning depends on order type. + :param leverage: Amount of desired leverage. + :param oflags: Comma separated flags: "viqc", "fcib", "fciq", "nompp". + :param starttm: Scheduled start time. Zero (now) or unix timestamp. + :param expiretm: Expiration time. Zero (never) or unix timestamp. + :param userref: User reference id. 32-bit signed number. + :param validate: Calidate inputs only. Do not submit order. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + if type not in ["buy", "sell"]: + raise ValueError("Parameter type is not valid") + + if ordertype not in [ + "market" "stop-loss", "take-profit", "stop-loss-profit", "stop-loss-profit-limit", + "stop-loss-limit", "take-profit-limit", "trailing-stop", "trailing-stop-limit", + "stop-loss-and-limit", "settle-position" + ]: + + raise ValueError("Parameter ordertype is not valid") + + parameters = { + "pair": pair, + "type": otype, + "ordertype": ordertype, + "price": price, + "volume": volume, + "leverage": leverage, + "starttm": starttm, + "expiretm": expiretm, + } + + if price2 != -1: + parameters["price2"] = price2 + + if len(oflags) != 0: + parameters["oflags"] = oflags + + if len(userref) != 0: + parameters["userref"] = userref + + if validate: + parameters["validate"] = "true" + + return self.private("AddOrder", parameters) + + def cancel_order(self, txid): + """ + Cancels order. See `Kraken documentation `__. + :param txid: Transaction id. + :return: Response as JSON object. + :raises: Any of private method exceptions. + """ + parameters = { + "txid": txid + } + + return self.private("CancelOrder", parameters) + +class ConnectionTimeout(Exception): + + pass + \ No newline at end of file diff --git a/blankly/exchanges/interfaces/kraken/kraken_auth.py b/blankly/exchanges/interfaces/kraken/kraken_auth.py new file mode 100644 index 00000000..05536f8e --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/kraken_auth.py @@ -0,0 +1,8 @@ +from blankly.exchanges.auth.abc_auth import ABCAuth + + +class KrakenAuth(ABCAuth): + def __init__(self, keys_file, portfolio_name): + super().__init__(keys_file, portfolio_name, 'kraken') + needed_keys = ['API_KEY', 'API_SECRET'] + self.validate_credentials(needed_keys) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py new file mode 100644 index 00000000..4d6153c1 --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -0,0 +1,349 @@ +import time + +import pandas as pd +from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI +from typing import List +import warnings + +import blankly.utils.time_builder +import blankly.utils.utils as utils +import blankly.exchanges.interfaces.kraken.kraken_interface_utils as interface_utils +from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface +from blankly.exchanges.orders.limit_order import LimitOrder +from blankly.exchanges.orders.market_order import MarketOrder +from blankly.exchanges.orders.stop_limit import StopLimit +from blankly.utils.exceptions import APIException, InvalidOrder + +class KrakenInterface(ExchangeInterface): + + #NOTE, kraken has no sandbox mode + def __init__(self, authenticated_API: KrakenAPI, preferences_path: str): + super().__init__('kraken', authenticated_API, preferences_path, valid_resolutions=None) + + self.account_levels_to_max_open_orders = { + "Starter": 60, + "Express": 60, + "Intermediate": 80, + "Pro": 225 + } + + def init_exchange(self): + pass + + """ + 'get_products': [ + ["symbol", str], + ["base_asset", str], + ["quote_asset", str], + ["base_min_size", float], + ["base_max_size", float], + ["base_increment", float] + ], + """ + + """ + + NOTE: + This method might behave incorrectly as it sets the symbol as + wsname (returned by Kraken API). This was chosen as it most + closely fits in with the symbol names used by the rest of the + package, but may be less stable as it may or may not be + intended to be fully relied upon. + + """ + + def get_products(self) -> list: + + needed_asset_pairs = [] + needed = self.needed["get_products"] + asset_pairs: List[dict] = self.get_calls().asset_pairs() + for asset_id, asset_pair in asset_pairs.items(): + asset_pair["symbol"] = asset_pair["wsname"].replace('/', '-') + asset_pair["base_asset"] = asset_pair.pop("base") + asset_pair["quote_asset"] = asset_pair.pop("quote") + asset_pair["base_min_size"] = asset_pair.pop("ordermin") + asset_pair["base_max_size"] = 99999999999 + asset_pair["base_increment"] = 10**(-1 * float(asset_pair.pop("lot_decimals"))) + asset_pair["kraken_id"] = asset_id + needed_asset_pairs.append(utils.isolate_specific(needed, asset_pair)) + + return needed_asset_pairs + + #NOTE: implement this + def get_account(self): + pass + + """ + Needed: + 'market_order': [ + ["symbol", str], + ["id", str], + ["created_at", float], + ["size", float], + ["status", str], + ["type", str], + ["side", str] + ], + """ + + def market_order(self, symbol: str, side: str, size: float): + symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + response = self.get_calls().add_order(symbol, side, "market", 0, size) + + txid = response["txid"] + order_info = self.get_calls().query_orders(txid=txid) + + order_info["symbol"] = symbol + order_info["id"] = txid + order_info["created_at"] = order_info.pop("opentm") + order_info["size"] = size + order_info["type"] = "market" + order_info["side"] = order_info["descr"]["type"] + + order = { + 'size': size, + 'side': side, + 'symbol': symbol, + 'type': 'market' + } + + return MarketOrder(order, order_info, self) + + def limit_order(self, symbol: str, side: str, price: float, size: float): + symbol = interface_utils(symbol) + response = self.get_calls().add_order(symbol, side, "market", price, size) + txid = response["txid"] + order_info = self.get_calls().query_orders(txid=txid) + + order_info["symbol"] = symbol + order_info["id"] = txid + order_info["created_at"] = order_info.pop("opentm") + order_info["size"] = size + order_info["type"] = "market" + order_info["side"] = order_info["descr"]["type"] + + order = { + 'size': size, + 'side': side, + 'symbol': symbol, + 'type': 'market' + } + + return LimitOrder(order, order_info, self) + + def cancel_order(self, symbol: str, order_id: str): + return self.get_calls().cancel_order(order_id) + + + def get_open_orders(self, symbol: str = None) -> list: + """ + List open orders. + Args: + symbol (optional) (str): Asset such as BTC-USD + """ + response = self.get_calls().open_orders(symbol)["open"] + response_needed_fulfilled = [] + if(len(response) == 0): + return [] + + for open_order_id, open_order_info in response.items(): + utils.pretty_print_JSON(f"json: {response}", actually_print=True) + + + + # Needed: + # 'get_open_orders': [ # Key specificity changes based on order type + # ["id", str], + # ["price", float], + # ["size", float], + # ["type", str], + # ["side", str], + # ["status", str], + # ["product_id", str] + # ], + + open_order_info['id'] = open_order_id + open_order_info["size"] = open_order_info.pop("vol") + open_order_info["type"] = open_order_info["descr"].pop("ordertype") + open_order_info["side"] = open_order_info["descr"].pop("type") + open_order_info['product_id'] = interface_utils.kraken_symbol_to_blankly_symbol(open_order_info["descr"].pop('pair')) + open_order_info['created_at'] = open_order_info.pop('opentm') + + if open_order_info["type"] == "limit": + needed = self.choose_order_specificity("limit") + open_order_info['time_in_force'] = "GTC" + open_order_info['price'] = float(open_order_info["descr"].pop("price")) + elif open_order_info["type"] == "market": + needed = self.choose_order_specificity("market") + + + open_order_info = utils.isolate_specific(needed, open_order_info) + + response_needed_fulfilled.append(open_order_info) + + return response_needed_fulfilled + + + # 'get_order': [ + # ["product_id", str], + # ["id", str], + # ["price", float], + # ["size", float], + # ["type", str], + # ["side", str], + # ["status", str], + # ["funds", float] + # ], + + def get_order(self, symbol: str, order_id: str) -> dict: + """ + Get a certain order + Args: + symbol: Asset that the order is under + order_id: The unique ID of the order. + """ + response = self.get_calls().query_orders(txid=order_id)[order_id] + utils.pretty_print_JSON(response, actually_print=True) + + order_type = response["descr"].pop("ordertype") + + needed = self.choose_order_specificity(order_type) + + response["id"] = order_id + response['type'] = order_type + response['symbol'] = interface_utils.kraken_symbol_to_blankly_symbol(response['descr']['pair']) + response['created_at'] = response.pop('opentm') + response['price'] = response['descr'].pop('price') + response['size'] = response.pop("vol") + response['side'] = response["descr"].pop("type") + #NOTE, what is funds in needed? + + if order_type == "limit": + response['time_in_force'] = "GTC" + + response = utils.isolate_specific(needed, response) + return response + + #NOTE fees are dependent on asset pair, so this is a required parameter to call the function + def get_fees(self, asset_symbol: str, size: float) -> dict: + asset_pair_info = self.get_calls().asset_pairs() + + fees_maker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees_maker"] + fees_taker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees"] + + for volume_fee in reversed(fees_maker): + if size > volume_fee[0]: + fee_maker = volume_fee[1] + + for volume_fee in reversed(fees_taker): + if size > volume_fee[0]: + fee_taker = volume_fee[1] + + return { + "maker_fee_rate": fee_maker, + "taker_fee_rate": fee_taker, + } + + + + def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd.DataFrame: + + symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + + accepted_grans = [60, 300, 900, 1800, 3600, 14400, 86400, 604800, 1296000] + + if resolution not in accepted_grans: + utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.") + resolution = accepted_grans[min(range(len(accepted_grans)), + key=lambda i: abs(accepted_grans[i] - resolution))] + + #kraken processes resolution in seconds, so the granularity must be divided by 60 + product_history = self.get_calls().ohlc(symbol, interval = (resolution / 60), since = epoch_start) + + historical_data_raw = product_history[symbol] + historical_data_block = [] + num_intervals = len(historical_data_raw) + + for i, interval in enumerate(historical_data_raw): + + time = interval[0] + open_ = interval[1] + high = interval[2] + low = interval[3] + close = interval[4] + volume = interval[5] + + if time > epoch_stop: + break + + historical_data_block.append([time, low, high, open_, close, volume]) + + utils.update_progress(i / num_intervals) + + + + print("\n") + df = pd.DataFrame(historical_data_block, columns=['time', 'low', 'high', 'open', 'close', 'volume']) + df_start = df["time"][0] + if df_start > epoch_start: + warnings.warn(f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df_start}") + + return df + + def get_order_filter(self, symbol: str) -> dict: + + + kraken_symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + + asset_info = self.get_calls().asset_pairs(pairs = kraken_symbol) + + price = self.get_calls().ticker(kraken_symbol)[kraken_symbol]["c"][0] + return { + "symbol": symbol, + "base_asset": asset_info[kraken_symbol]["wsname"].split("/")[0], + "quote_asset": asset_info[kraken_symbol]["wsname"].split("/")[1], + "max_orders": self.account_levels_to_max_open_orders[self.user_preferences["settings"]["kraken"]["account_type"]], + "limit_order": { + "base_min_size": asset_info[kraken_symbol]["ordermin"], + "base_max_size": 999999999, + "base_increment": asset_info[kraken_symbol]["lot_decimals"], + "price_increment": asset_info[kraken_symbol]["pair_decimals"], + "min_price": float(asset_info[kraken_symbol]["ordermin"]) * float(price), + "max_price": 999999999, + }, + "market_order": { + "base_min_size": asset_info[kraken_symbol]["ordermin"], + "base_max_size": 999999999, + "base_increment": asset_info[kraken_symbol]["lot_decimals"], + "quote_increment": asset_info[kraken_symbol]["pair_decimals"], + "buy": { + "min_funds": 0, + "max_funds": 999999999, + }, + "sell": { + "min_funds": 0, + "max_funds": 999999999 + }, + + }, + "exchange_specific": {} + + } + + + + def get_price(self, symbol: str) -> float: + """ + Returns just the price of a symbol. + Args: + symbol: The asset such as (BTC-USD, or MSFT) + """ + symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + + resp = self.get_calls().ticker(symbol) + + opening_price = float(resp[symbol]["o"]) + volume_weighted_average_price = float(resp[symbol]["p"][0]) + last_price = float(resp[symbol]["c"][0]) + + return volume_weighted_average_price \ No newline at end of file diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py b/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py new file mode 100644 index 00000000..4bb788a6 --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py @@ -0,0 +1,37 @@ +import json +from pathlib import Path +from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI +from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth + + +def kraken_api() -> KrakenAPI: + keys_file_path = Path("keys.json").resolve() + + auth_obj = KrakenAuth(str(keys_file_path), "default") + api = KrakenAPI(auth_obj) + return api + + +def blankly_symbol_to_kraken_symbol(blankly_symbol: str): + api = kraken_api() + response = api.asset_pairs() + + + for key, value in response.items(): + + wsname = value["wsname"].replace("/", "-") + + if wsname == blankly_symbol: + return key + + raise ValueError("Invalid blankly symbol") + +def kraken_symbol_to_blankly_symbol(kraken_symbol: str): + api = kraken_api() + response = api.asset_pairs() + + for key, value in response.items(): + if key == kraken_symbol or value["altname"] == kraken_symbol: + return value["wsname"].replace("/", "-") + + raise ValueError("Invalid kraken symbol") \ No newline at end of file diff --git a/blankly/exchanges/interfaces/kraken/kraken_websocket.py b/blankly/exchanges/interfaces/kraken/kraken_websocket.py new file mode 100644 index 00000000..19f812ae --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/kraken_websocket.py @@ -0,0 +1,199 @@ +import collections +import json +import ssl +import threading +import time +import traceback + +from websocket import create_connection + +import blankly +import blankly.exchanges.interfaces.kraken.kraken_websocket_utils as websocket_utils +from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket + + +def create_ticker_connection(id, url, channel): + ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE}) + request = json.dumps({ + "op": "subscribe", + "channel": channel, + "market": id + }) + ws.send(request) + return ws + + +# This could be needed: +# "channels": [ +# { +# "name": "ticker", +# "product_ids": [ +# \"""" + id + """\" +# ] +# } +# ] + + +class Tickers(ABCExchangeWebsocket): + def __init__(self, symbol, stream, log=None, + pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="ws.kraken.com"): + """ + Create and initialize the ticker + Args: + symbol: Currency to initialize on such as "BTC-USD" + log: Fill this with a path to a log file that should be created + WEBSOCKET_URL: Default websocket URL feed. + """ + self.__id = symbol + self.__stream = stream + self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream) + + # Initialize log file + if log is not None: + self.__log = True + self.__filePath = log + try: + self.__file = open(log, 'x+') + self.__file.write(log_message) + except FileExistsError: + self.__file = open(log, 'a') + else: + self.__log = False + + self.URL = WEBSOCKET_URL + self.ws = None + self.__response = None + self.__most_recent_tick = None + self.__most_recent_time = None + self.__callbacks = [] + self.__pre_event_callback = pre_event_callback + + # Reload preferences + self.__preferences = blankly.utils.load_user_preferences() + buffer_size = self.__preferences["settings"]["websocket_buffer_size"] + self.__ticker_feed = collections.deque(maxlen=buffer_size) + self.__time_feed = collections.deque(maxlen=buffer_size) + + # Start the websocket + if not initially_stopped: + self.start_websocket() + + def start_websocket(self): + """ + Restart websocket if it was asked to stop. + """ + if self.ws is None: + self.ws = create_ticker_connection(self.__id, self.URL, self.__stream) + self.__response = self.ws.recv() + thread = threading.Thread(target=self.read_websocket) + thread.start() + else: + if self.ws.connected: + print("Already running...") + pass + else: + # Use recursion to restart, continue appending to time feed and ticker feed + self.ws = None + self.start_websocket() + + def read_websocket(self): + counter = 0 + # TODO port this to "WebSocketApp" found in the websockets documentation + while self.ws.connected: + # In case the user closes while its reading from the websocket, this will let it expire + persist_connected = self.ws.connected + try: + received_string = self.ws.recv() + received_dict = json.loads(received_string) + parsed_received_trades = websocket_utils.process_trades(received_dict) + for received in parsed_received_trades: + #ISO8601 is converted to epoch in process_trades + self.__most_recent_time = received["time"] + self.__time_feed.append(self.__most_recent_time) + + if self.__log: + if counter % 100 == 0: + self.__file.close() + self.__file = open(self.__filePath, 'a') + line = self.__logging_callback(received) + self.__file.write(line) + + # Manage price events and fire for each manager attached + interface_message = self.__interface_callback(received) + self.__ticker_feed.append(interface_message) + self.__most_recent_tick = interface_message + + try: + for i in self.__callbacks: + i(interface_message) + except Exception: + traceback.print_exc() + + counter += 1 + except Exception as e: + if persist_connected: + traceback.print_exc() + pass + else: + traceback.print_exc() + print("Error reading ticker websocket for " + self.__id + " on " + + self.__stream + ": attempting to re-initialize") + # Give a delay so this doesn't eat up from the main thread if it takes many tries to initialize + time.sleep(2) + self.ws.close() + self.ws = create_ticker_connection(self.__id, self.URL, self.__stream) + # Update response + self.__response = self.ws.recv() + + """ Required in manager """ + + def is_websocket_open(self): + return self.ws.connected + + def get_currency_id(self): + return self.__id + + """ Required in manager """ + + def append_callback(self, obj): + self.__callbacks.append(obj) + + """ Define a variable each time so there is no array manipulation """ + """ Required in manager """ + + def get_most_recent_tick(self): + return self.__most_recent_tick + + """ Required in manager """ + + def get_most_recent_time(self): + return self.__most_recent_time + + """ Required in manager """ + + def get_time_feed(self): + return list(self.__time_feed) + + """ Parallel with time feed """ + """ Required in manager """ + + def get_feed(self): + return list(self.__ticker_feed) + + """ Required in manager """ + + def get_response(self): + return self.__response + + """ Required in manager """ + + def close_websocket(self): + if self.ws.connected: + self.ws.close() + else: + print("Websocket for " + self.__id + ' on channel ' + self.__stream + " is already closed") + + """ Required in manager """ + + def restart_ticker(self): + self.start_websocket() diff --git a/blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py b/blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index e768ee20..9652e952 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ 'python-binance', 'requests', 'websocket-client', + 'ciso8601' ], classifiers=[ # Possible: "3 - Alpha", "4 - Beta" or "5 - Production/Stable" diff --git a/tests/exchanges/interfaces/kraken/test_kraken_interface.py b/tests/exchanges/interfaces/kraken/test_kraken_interface.py new file mode 100644 index 00000000..a384d84e --- /dev/null +++ b/tests/exchanges/interfaces/kraken/test_kraken_interface.py @@ -0,0 +1,149 @@ +""" + +ATTENTION: +Kraken has NO sandbox mode, these are methods specifically designed +to test interface methods which interact with LIVE orders, and +move REAL funds. It is not recommended that you run these tests. +They must be executed individually and with great caution. Blankly +is not responsible for losses incurred by improper use of this test. + +How do we test methods which affect live funds? + +There are 5 main methods that need to be tested which involve +using live funds + +1. get_open_orders() +2. market_order() +3. limit_order() +4. cancel_order + +We test these by creating a series of assertations that can only +pass if everything works properly. + +""" + + +import time +import sys +from blankly.exchanges.orders.limit_order import LimitOrder +from blankly.utils import utils +from pathlib import Path +from typing import List + +from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth +from blankly.exchanges.interfaces.kraken.kraken_interface import KrakenInterface +from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory +import test_kraken_interface_utils as test_utils + + + +def kraken_interface() -> KrakenInterface: + keys_file_path = Path("tests/config/keys.json").resolve() + settings_file_path = Path("tests/config/settings.json").resolve() + + auth_obj = KrakenAuth(str(keys_file_path), "default") + _, kraken_interface = DirectCallsFactory.create("kraken", auth_obj, str(settings_file_path)) + return kraken_interface + +""" +cancels ALL open orders and distributes funds for testing purposes + +pass command line argument reset to activate + +use with extreme caution + +""" +def reset(kraken_interface: KrakenInterface): + validation = input("Continuing with this test will cancel ALL existing open orders and redistribute funds for testing purposes. Continue (Y/n)? ") + if validation.lower() != "y": + print("aborting test") + quit() + + for order in kraken_interface.get_open_orders(): + kraken_interface.cancel_order("BTC-USD", order["id"]) + + +def main(): + + interface: KrakenInterface = kraken_interface() + + validation = input("To complete this test properly, you should have around $10 in USD in your Kraken account.\nThis test will involve the conversion of no more than $10 to BTC.\nDo not have any trades going on while completing this test.\nContinue (Y/n)? ") + if validation.lower() != "y": + quit() + + if len(sys.argv) == 2: + if sys.argv[1] == "reset": + reset(interface) + else: + print("Invalid command line argument. Aborting") + + ### start testing ### + + + initial_num_open_orders = len(interface.get_open_orders()) + + #creates a buy limit order unlikely to be fulfilled (buying a bitcoin for $5) + limit_order = interface.limit_order("BTC-USD", "buy", 1, 5) + assert len(interface.get_open_orders()) == 1 + initial_num_open_orders, f"expected {1 + initial_num_open_orders} but got {len(interface.get_open_orders())}" + assert limit_order.get_status()['status'] == "new" or limit_order.get_status()['status'] == "open", f"expected open but got {limit_order.get_status()['status']} of type {type(limit_order.get_status()['status'])}" + + open_order_ids: List[int] = test_utils.get_open_order_ids(interface.get_open_orders()) + limit_order_id = int(limit_order.get_id()) + assert limit_order_id in open_order_ids, f"id {limit_order_id} (type: {type(limit_order_id)}) not in list {open_order_ids} (type: {type(open_order_ids)})" + + #cancels the open limit order + order_id = int(interface.cancel_order("BTC-USD", limit_order.get_id())) + + curr_num_open_orders = len(interface.get_open_orders()) + assert curr_num_open_orders == initial_num_open_orders, f"expected {initial_num_open_orders} open orders, but got {curr_num_open_orders}\nopen order list:\n{interface.get_open_orders()}" + assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders()) + + #creates a buy market order for $5 worth of bitcoin + market_order = interface.market_order("BTC-USD", "buy", 5 / interface.get_price("BTC-USD")) + + while market_order.get_status() == "open": + print("Waiting for test market order to get accepted...", end = '\r') + time.sleep(1) + + print("Test market order accepted... continuing test") + + market_order_info = interface.get_order("BTC-USD", market_order.get_id()) + + assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders()) + assert market_order_info["id"] == market_order.get_id() + market_order_status = market_order_info["status"] + assert market_order_status == "closed", f"expected closed, but got {market_order_status} (type: {type(market_order_status)}) \nopen orders: {interface.get_open_orders()}" + + #creates a sell limit order unlikely to get fulfilled (selling $5 worth of bitcoin for $5000) + limit_order = interface.limit_order("BTC-USD", "sell", 5000 * (interface.get_price("BTC-USD") / 5), interface.get_price("BTC-USD") / 5) + + assert limit_order.get_id() in test_utils.get_open_order_ids(interface.get_open_orders()) + limit_order_info = interface.get_order("BTC-USD", limit_order.get_id()) + assert limit_order_info["id"] == limit_order.get_id() + assert limit_order_info["status"] == "open" + assert len(interface.get_open_orders()) == initial_num_open_orders + 1 + + #cancels sell limit order + order_id = interface.cancel_order("BTC-USD", limit_order.get_id()) + assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders()) + assert len(interface.get_open_orders()) == initial_num_open_orders + + #creates sell market order + market_order = interface.market_order("BTC-USD", "sell", 5 / interface.get_price("BTC-USD")) + + while market_order.get_status() == "open": + print("Waiting for test market order to get accepted...", end = '\r') + time.sleep(1) + + print("Test market order accepted... continuing test") + + market_order_info = interface.get_order("BTC-USD", market_order.get_id()) + + assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders()) + assert market_order_info["id"] == market_order.get_id() + assert market_order_info["status"] == "closed" + + ### end testing ### + +if __name__ == "__main__": + main() \ No newline at end of file From 7a717b120eb572c6befac44cee0bc5c9eefceaaf Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 8 Feb 2022 23:39:12 -0500 Subject: [PATCH 02/13] test commit on aydin's branch --- tests/exchanges/interfaces/kraken/helloworld.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/exchanges/interfaces/kraken/helloworld.py diff --git a/tests/exchanges/interfaces/kraken/helloworld.py b/tests/exchanges/interfaces/kraken/helloworld.py new file mode 100644 index 00000000..e75154b7 --- /dev/null +++ b/tests/exchanges/interfaces/kraken/helloworld.py @@ -0,0 +1 @@ +print("hello world") \ No newline at end of file From ab753b9a302dcf34a1c268990d3868bbcecfd5f8 Mon Sep 17 00:00:00 2001 From: Avi Date: Sat, 12 Feb 2022 15:30:42 -0500 Subject: [PATCH 03/13] testing kraken functions --- .../interfaces/kraken/kraken_interface.py | 30 ++++++++++++------- .../kraken/kraken_interface_utils.py | 2 +- tests/config/settings.json | 3 ++ .../kraken/test_kraken_interface_utils.py | 10 +++++++ tests/exchanges/test_interface_homogeneity.py | 8 +++++ tests/testing_utils.py | 2 ++ 6 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 tests/exchanges/interfaces/kraken/test_kraken_interface_utils.py diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 4d6153c1..2f1c318f 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -225,11 +225,16 @@ def get_order(self, symbol: str, order_id: str) -> dict: return response #NOTE fees are dependent on asset pair, so this is a required parameter to call the function - def get_fees(self, asset_symbol: str, size: float) -> dict: + def get_fees(self) -> dict: + asset_symbol = "XBTUSDT" + size = 1 asset_pair_info = self.get_calls().asset_pairs() - - fees_maker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees_maker"] - fees_taker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees"] + + fees_maker: List[List[float]] = asset_pair_info[(asset_symbol)]["fees_maker"] + fees_taker: List[List[float]] = asset_pair_info[(asset_symbol)]["fees"] + + # fees_maker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees_maker"] + # fees_taker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees"] for volume_fee in reversed(fees_maker): if size > volume_fee[0]: @@ -248,7 +253,7 @@ def get_fees(self, asset_symbol: str, size: float) -> dict: def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd.DataFrame: - symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + #symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) accepted_grans = [60, 300, 900, 1800, 3600, 14400, 86400, 604800, 1296000] @@ -291,13 +296,17 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd return df def get_order_filter(self, symbol: str) -> dict: - - - kraken_symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + + symbol = "XBTUSDT" + kraken_symbol = symbol + #kraken_symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) asset_info = self.get_calls().asset_pairs(pairs = kraken_symbol) price = self.get_calls().ticker(kraken_symbol)[kraken_symbol]["c"][0] + + # max_orders = self.account_levels_to_max_open_orders[self.user_preferences["settings"]["kraken"]["account_type"]] + return { "symbol": symbol, "base_asset": asset_info[kraken_symbol]["wsname"].split("/")[0], @@ -338,7 +347,8 @@ def get_price(self, symbol: str) -> float: Args: symbol: The asset such as (BTC-USD, or MSFT) """ - symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + symbol = "XBTUSDT" + # symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) resp = self.get_calls().ticker(symbol) @@ -346,4 +356,4 @@ def get_price(self, symbol: str) -> float: volume_weighted_average_price = float(resp[symbol]["p"][0]) last_price = float(resp[symbol]["c"][0]) - return volume_weighted_average_price \ No newline at end of file + return volume_weighted_average_price \ No newline at end of file diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py b/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py index 4bb788a6..ef84d5c9 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py @@ -5,7 +5,7 @@ def kraken_api() -> KrakenAPI: - keys_file_path = Path("keys.json").resolve() + keys_file_path = Path("./tests/config/keys.json") auth_obj = KrakenAuth(str(keys_file_path), "default") api = KrakenAPI(auth_obj) diff --git a/tests/config/settings.json b/tests/config/settings.json index 64a3115d..c0c5a6f5 100644 --- a/tests/config/settings.json +++ b/tests/config/settings.json @@ -19,6 +19,9 @@ }, "oanda": { "cash": "USD" + }, + "kraken": { + "account_type": "Starter" } } } \ No newline at end of file diff --git a/tests/exchanges/interfaces/kraken/test_kraken_interface_utils.py b/tests/exchanges/interfaces/kraken/test_kraken_interface_utils.py new file mode 100644 index 00000000..bbbcf60f --- /dev/null +++ b/tests/exchanges/interfaces/kraken/test_kraken_interface_utils.py @@ -0,0 +1,10 @@ +from typing import List + + +def get_open_order_ids(open_orders: List[dict]) -> List[str]: + ids: List[str] = [] + + for order in open_orders: + ids.append(str(order["id"])) + + return ids diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py index 096b6a61..2cae6b19 100644 --- a/tests/exchanges/test_interface_homogeneity.py +++ b/tests/exchanges/test_interface_homogeneity.py @@ -72,6 +72,14 @@ def setUpClass(cls) -> None: cls.Binance_Interface_data = cls.Binance_data.get_interface() cls.data_interfaces.append(cls.Binance_Interface_data) + #kraken definition and appending + cls.Kraken = blankly.Kraken(portfolio_name="default", + keys_path='./tests/config/keys.json', + settings_path="./tests/config/settings.json") + cls.Kraken_Interface = cls.Kraken.get_interface() + cls.interfaces.append(cls.Kraken_Interface) + cls.data_interfaces.append(cls.Kraken_Interface) + # alpaca definition and appending cls.alpaca = blankly.Alpaca(portfolio_name="alpaca test portfolio", keys_path='./tests/config/keys.json', diff --git a/tests/testing_utils.py b/tests/testing_utils.py index e9e5b603..b89e0759 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -28,5 +28,7 @@ def get_valid_symbol(exchange: str): return 'EUR-USD' elif exchange == 'ftx': return 'BTC-USD' + elif exchange == 'kraken': + return 'BTC-USD' else: raise LookupError("Specified exchange not found.") From 2bfdd56c03c4d562a5af3e51b189ad4b42650e4e Mon Sep 17 00:00:00 2001 From: Avi Date: Mon, 14 Feb 2022 13:31:27 -0500 Subject: [PATCH 04/13] 7/12 kraken functions passing --- .../interfaces/kraken/kraken_interface.py | 268 ++++++++++------ .../kraken/test_kraken_interface.py | 298 +++++++++--------- tests/exchanges/test_interface_homogeneity.py | 16 +- 3 files changed, 333 insertions(+), 249 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 2f1c318f..555fcbd5 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -14,22 +14,23 @@ from blankly.exchanges.orders.stop_limit import StopLimit from blankly.utils.exceptions import APIException, InvalidOrder + class KrakenInterface(ExchangeInterface): - - #NOTE, kraken has no sandbox mode + + # NOTE, kraken has no sandbox mode def __init__(self, authenticated_API: KrakenAPI, preferences_path: str): super().__init__('kraken', authenticated_API, preferences_path, valid_resolutions=None) - + self.account_levels_to_max_open_orders = { "Starter": 60, "Express": 60, "Intermediate": 80, "Pro": 225 } - + def init_exchange(self): pass - + """ 'get_products': [ ["symbol", str], @@ -40,7 +41,7 @@ def init_exchange(self): ["base_increment", float] ], """ - + """ NOTE: @@ -51,9 +52,9 @@ def init_exchange(self): intended to be fully relied upon. """ - + def get_products(self) -> list: - + needed_asset_pairs = [] needed = self.needed["get_products"] asset_pairs: List[dict] = self.get_calls().asset_pairs() @@ -63,16 +64,100 @@ def get_products(self) -> list: asset_pair["quote_asset"] = asset_pair.pop("quote") asset_pair["base_min_size"] = asset_pair.pop("ordermin") asset_pair["base_max_size"] = 99999999999 - asset_pair["base_increment"] = 10**(-1 * float(asset_pair.pop("lot_decimals"))) + asset_pair["base_increment"] = 10 ** (-1 * float(asset_pair.pop("lot_decimals"))) asset_pair["kraken_id"] = asset_id needed_asset_pairs.append(utils.isolate_specific(needed, asset_pair)) - + return needed_asset_pairs - - #NOTE: implement this - def get_account(self): - pass - + + # NOTE: implement this + def get_account(self, symbol=None): + symbol = super().get_account(symbol) + + positions = self.calls.list_positions() + positions_dict = utils.AttributeDict({}) + + for position in positions: + curr_symbol = position.pop('symbol') + positions_dict[curr_symbol] = utils.AttributeDict({ + 'available': float(position.pop('vol')), + 'hold': 0.0 + }) + + symbols = list(positions_dict.keys()) + # Catch an edge case bug that if there are no positions it won't try to snapshot + if len(symbols) != 0: + open_orders = self.calls.list_orders(status='open', symbols=symbols) + snapshot_price = self.calls.get_snapshots(symbols=symbols) + else: + open_orders = [] + snapshot_price = {} + + # now grab the available cash in the account + account = self.calls.get_account() + positions_dict['USD'] = utils.AttributeDict({ + 'available': float(account['buying_power']), + 'hold': 0.0 + }) + + for order in open_orders: + curr_symbol = order['symbol'] + if order['side'] == 'buy': # buy orders only affect USD holds + if order['qty']: # this case handles qty market buy and limit buy + if order['type'] == 'limit': + dollar_amt = float(order['qty']) * float(order['limit_price']) + elif order['type'] == 'market': + dollar_amt = float(order['qty']) * snapshot_price[curr_symbol]['latestTrade']['p'] + else: # we don't have support for stop_order, stop_limit_order + dollar_amt = 0.0 + else: # this is the case for notional market buy + dollar_amt = float(order['notional']) + + # In this case we don't have to subtract because the buying power is the available money already + # we just need to add to figure out how much is actually on limits + # positions_dict['USD']['available'] -= dollar_amt + + # So just add to our hold + positions_dict['USD']['hold'] += dollar_amt + + else: + if order['qty']: # this case handles qty market sell and limit sell + qty = float(order['qty']) + else: # this is the case for notional market sell, calculate the qty with cash/price + qty = float(order['notional']) / snapshot_price[curr_symbol]['latestTrade']['p'] + + positions_dict[curr_symbol]['available'] -= qty + positions_dict[curr_symbol]['hold'] += qty + + # Note that now __unique assets could be uninitialized: + if self.__unique_assets is None: + self.init_exchange() + + for i in self.__unique_assets: + if i not in positions_dict: + positions_dict[i] = utils.AttributeDict({ + 'available': 0.0, + 'hold': 0.0 + }) + + if symbol is not None: + if symbol in positions_dict: + return utils.AttributeDict({ + 'available': float(positions_dict[symbol]['available']), + 'hold': float(positions_dict[symbol]['hold']) + }) + else: + raise KeyError('Symbol not found.') + + if symbol == 'USD': + return utils.AttributeDict({ + 'available': positions_dict['USD']['available'], + 'hold': positions_dict['USD']['hold'] + }) + + return positions_dict + + def market_order(self, symbol: str, side: str, size: float): """ Needed: 'market_order': [ @@ -85,21 +170,19 @@ def get_account(self): ["side", str] ], """ - - def market_order(self, symbol: str, side: str, size: float): symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) response = self.get_calls().add_order(symbol, side, "market", 0, size) - + txid = response["txid"] order_info = self.get_calls().query_orders(txid=txid) - + order_info["symbol"] = symbol order_info["id"] = txid order_info["created_at"] = order_info.pop("opentm") order_info["size"] = size order_info["type"] = "market" order_info["side"] = order_info["descr"]["type"] - + order = { 'size': size, 'side': side, @@ -108,33 +191,32 @@ def market_order(self, symbol: str, side: str, size: float): } return MarketOrder(order, order_info, self) - + def limit_order(self, symbol: str, side: str, price: float, size: float): symbol = interface_utils(symbol) - response = self.get_calls().add_order(symbol, side, "market", price, size) + response = self.get_calls().add_order(symbol, side, "market", price, size) txid = response["txid"] order_info = self.get_calls().query_orders(txid=txid) - + order_info["symbol"] = symbol order_info["id"] = txid order_info["created_at"] = order_info.pop("opentm") order_info["size"] = size order_info["type"] = "market" order_info["side"] = order_info["descr"]["type"] - + order = { 'size': size, 'side': side, 'symbol': symbol, 'type': 'market' } - + return LimitOrder(order, order_info, self) - + def cancel_order(self, symbol: str, order_id: str): return self.get_calls().cancel_order(order_id) - - + def get_open_orders(self, symbol: str = None) -> list: """ List open orders. @@ -143,14 +225,12 @@ def get_open_orders(self, symbol: str = None) -> list: """ response = self.get_calls().open_orders(symbol)["open"] response_needed_fulfilled = [] - if(len(response) == 0): + if (len(response) == 0): return [] for open_order_id, open_order_info in response.items(): utils.pretty_print_JSON(f"json: {response}", actually_print=True) - - - + # Needed: # 'get_open_orders': [ # Key specificity changes based on order type # ["id", str], @@ -161,14 +241,15 @@ def get_open_orders(self, symbol: str = None) -> list: # ["status", str], # ["product_id", str] # ], - + open_order_info['id'] = open_order_id open_order_info["size"] = open_order_info.pop("vol") open_order_info["type"] = open_order_info["descr"].pop("ordertype") open_order_info["side"] = open_order_info["descr"].pop("type") - open_order_info['product_id'] = interface_utils.kraken_symbol_to_blankly_symbol(open_order_info["descr"].pop('pair')) + open_order_info['product_id'] = interface_utils.kraken_symbol_to_blankly_symbol( + open_order_info["descr"].pop('pair')) open_order_info['created_at'] = open_order_info.pop('opentm') - + if open_order_info["type"] == "limit": needed = self.choose_order_specificity("limit") open_order_info['time_in_force'] = "GTC" @@ -176,25 +257,23 @@ def get_open_orders(self, symbol: str = None) -> list: elif open_order_info["type"] == "market": needed = self.choose_order_specificity("market") - open_order_info = utils.isolate_specific(needed, open_order_info) - + response_needed_fulfilled.append(open_order_info) - + return response_needed_fulfilled - - + # 'get_order': [ - # ["product_id", str], - # ["id", str], - # ["price", float], - # ["size", float], - # ["type", str], - # ["side", str], - # ["status", str], - # ["funds", float] - # ], - + # ["product_id", str], + # ["id", str], + # ["price", float], + # ["size", float], + # ["type", str], + # ["side", str], + # ["status", str], + # ["funds", float] + # ], + def get_order(self, symbol: str, order_id: str) -> dict: """ Get a certain order @@ -204,9 +283,9 @@ def get_order(self, symbol: str, order_id: str) -> dict: """ response = self.get_calls().query_orders(txid=order_id)[order_id] utils.pretty_print_JSON(response, actually_print=True) - + order_type = response["descr"].pop("ordertype") - + needed = self.choose_order_specificity(order_type) response["id"] = order_id @@ -216,15 +295,15 @@ def get_order(self, symbol: str, order_id: str) -> dict: response['price'] = response['descr'].pop('price') response['size'] = response.pop("vol") response['side'] = response["descr"].pop("type") - #NOTE, what is funds in needed? + # NOTE, what is funds in needed? if order_type == "limit": response['time_in_force'] = "GTC" response = utils.isolate_specific(needed, response) return response - - #NOTE fees are dependent on asset pair, so this is a required parameter to call the function + + # NOTE fees are dependent on asset pair, so this is a required parameter to call the function def get_fees(self) -> dict: asset_symbol = "XBTUSDT" size = 1 @@ -235,42 +314,40 @@ def get_fees(self) -> dict: # fees_maker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees_maker"] # fees_taker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees"] - + for volume_fee in reversed(fees_maker): if size > volume_fee[0]: fee_maker = volume_fee[1] - + for volume_fee in reversed(fees_taker): if size > volume_fee[0]: fee_taker = volume_fee[1] - + return { "maker_fee_rate": fee_maker, "taker_fee_rate": fee_taker, } - - - + def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd.DataFrame: - - #symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - + + # symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + symbol = "XBTUSD" accepted_grans = [60, 300, 900, 1800, 3600, 14400, 86400, 604800, 1296000] if resolution not in accepted_grans: utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.") resolution = accepted_grans[min(range(len(accepted_grans)), key=lambda i: abs(accepted_grans[i] - resolution))] - - #kraken processes resolution in seconds, so the granularity must be divided by 60 - product_history = self.get_calls().ohlc(symbol, interval = (resolution / 60), since = epoch_start) - + + # kraken processes resolution in seconds, so the granularity must be divided by 60 + product_history = self.get_calls().ohlc(symbol, interval=(resolution / 60), since=epoch_start) + symbol = "XXBTZUSD" historical_data_raw = product_history[symbol] historical_data_block = [] num_intervals = len(historical_data_raw) - + for i, interval in enumerate(historical_data_raw): - + time = interval[0] open_ = interval[1] high = interval[2] @@ -280,38 +357,45 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd if time > epoch_stop: break - + historical_data_block.append([time, low, high, open_, close, volume]) - + utils.update_progress(i / num_intervals) - - - + print("\n") df = pd.DataFrame(historical_data_block, columns=['time', 'low', 'high', 'open', 'close', 'volume']) df_start = df["time"][0] if df_start > epoch_start: - warnings.warn(f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df_start}") - + warnings.warn( + f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df_start}") + + df = df.astype({ + 'open': float, + 'high': float, + 'low': float, + 'close': float, + 'volume': float + }) return df - + def get_order_filter(self, symbol: str) -> dict: symbol = "XBTUSDT" kraken_symbol = symbol - #kraken_symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - - asset_info = self.get_calls().asset_pairs(pairs = kraken_symbol) - + # kraken_symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + + asset_info = self.get_calls().asset_pairs(pairs=kraken_symbol) + price = self.get_calls().ticker(kraken_symbol)[kraken_symbol]["c"][0] # max_orders = self.account_levels_to_max_open_orders[self.user_preferences["settings"]["kraken"]["account_type"]] return { - "symbol": symbol, + "symbol": symbol, "base_asset": asset_info[kraken_symbol]["wsname"].split("/")[0], "quote_asset": asset_info[kraken_symbol]["wsname"].split("/")[1], - "max_orders": self.account_levels_to_max_open_orders[self.user_preferences["settings"]["kraken"]["account_type"]], + "max_orders": self.account_levels_to_max_open_orders[ + self.user_preferences["settings"]["kraken"]["account_type"]], "limit_order": { "base_min_size": asset_info[kraken_symbol]["ordermin"], "base_max_size": 999999999, @@ -333,14 +417,12 @@ def get_order_filter(self, symbol: str) -> dict: "min_funds": 0, "max_funds": 999999999 }, - + }, "exchange_specific": {} - + } - - - + def get_price(self, symbol: str) -> float: """ Returns just the price of a symbol. @@ -349,11 +431,11 @@ def get_price(self, symbol: str) -> float: """ symbol = "XBTUSDT" # symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - + resp = self.get_calls().ticker(symbol) - + opening_price = float(resp[symbol]["o"]) volume_weighted_average_price = float(resp[symbol]["p"][0]) last_price = float(resp[symbol]["c"][0]) - - return volume_weighted_average_price \ No newline at end of file + + return volume_weighted_average_price diff --git a/tests/exchanges/interfaces/kraken/test_kraken_interface.py b/tests/exchanges/interfaces/kraken/test_kraken_interface.py index a384d84e..acfe7d07 100644 --- a/tests/exchanges/interfaces/kraken/test_kraken_interface.py +++ b/tests/exchanges/interfaces/kraken/test_kraken_interface.py @@ -1,149 +1,149 @@ -""" - -ATTENTION: -Kraken has NO sandbox mode, these are methods specifically designed -to test interface methods which interact with LIVE orders, and -move REAL funds. It is not recommended that you run these tests. -They must be executed individually and with great caution. Blankly -is not responsible for losses incurred by improper use of this test. - -How do we test methods which affect live funds? - -There are 5 main methods that need to be tested which involve -using live funds - -1. get_open_orders() -2. market_order() -3. limit_order() -4. cancel_order - -We test these by creating a series of assertations that can only -pass if everything works properly. - -""" - - -import time -import sys -from blankly.exchanges.orders.limit_order import LimitOrder -from blankly.utils import utils -from pathlib import Path -from typing import List - -from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth -from blankly.exchanges.interfaces.kraken.kraken_interface import KrakenInterface -from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory -import test_kraken_interface_utils as test_utils - - - -def kraken_interface() -> KrakenInterface: - keys_file_path = Path("tests/config/keys.json").resolve() - settings_file_path = Path("tests/config/settings.json").resolve() - - auth_obj = KrakenAuth(str(keys_file_path), "default") - _, kraken_interface = DirectCallsFactory.create("kraken", auth_obj, str(settings_file_path)) - return kraken_interface - -""" -cancels ALL open orders and distributes funds for testing purposes - -pass command line argument reset to activate - -use with extreme caution - -""" -def reset(kraken_interface: KrakenInterface): - validation = input("Continuing with this test will cancel ALL existing open orders and redistribute funds for testing purposes. Continue (Y/n)? ") - if validation.lower() != "y": - print("aborting test") - quit() - - for order in kraken_interface.get_open_orders(): - kraken_interface.cancel_order("BTC-USD", order["id"]) - - -def main(): - - interface: KrakenInterface = kraken_interface() - - validation = input("To complete this test properly, you should have around $10 in USD in your Kraken account.\nThis test will involve the conversion of no more than $10 to BTC.\nDo not have any trades going on while completing this test.\nContinue (Y/n)? ") - if validation.lower() != "y": - quit() - - if len(sys.argv) == 2: - if sys.argv[1] == "reset": - reset(interface) - else: - print("Invalid command line argument. Aborting") - - ### start testing ### - - - initial_num_open_orders = len(interface.get_open_orders()) - - #creates a buy limit order unlikely to be fulfilled (buying a bitcoin for $5) - limit_order = interface.limit_order("BTC-USD", "buy", 1, 5) - assert len(interface.get_open_orders()) == 1 + initial_num_open_orders, f"expected {1 + initial_num_open_orders} but got {len(interface.get_open_orders())}" - assert limit_order.get_status()['status'] == "new" or limit_order.get_status()['status'] == "open", f"expected open but got {limit_order.get_status()['status']} of type {type(limit_order.get_status()['status'])}" - - open_order_ids: List[int] = test_utils.get_open_order_ids(interface.get_open_orders()) - limit_order_id = int(limit_order.get_id()) - assert limit_order_id in open_order_ids, f"id {limit_order_id} (type: {type(limit_order_id)}) not in list {open_order_ids} (type: {type(open_order_ids)})" - - #cancels the open limit order - order_id = int(interface.cancel_order("BTC-USD", limit_order.get_id())) - - curr_num_open_orders = len(interface.get_open_orders()) - assert curr_num_open_orders == initial_num_open_orders, f"expected {initial_num_open_orders} open orders, but got {curr_num_open_orders}\nopen order list:\n{interface.get_open_orders()}" - assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders()) - - #creates a buy market order for $5 worth of bitcoin - market_order = interface.market_order("BTC-USD", "buy", 5 / interface.get_price("BTC-USD")) - - while market_order.get_status() == "open": - print("Waiting for test market order to get accepted...", end = '\r') - time.sleep(1) - - print("Test market order accepted... continuing test") - - market_order_info = interface.get_order("BTC-USD", market_order.get_id()) - - assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders()) - assert market_order_info["id"] == market_order.get_id() - market_order_status = market_order_info["status"] - assert market_order_status == "closed", f"expected closed, but got {market_order_status} (type: {type(market_order_status)}) \nopen orders: {interface.get_open_orders()}" - - #creates a sell limit order unlikely to get fulfilled (selling $5 worth of bitcoin for $5000) - limit_order = interface.limit_order("BTC-USD", "sell", 5000 * (interface.get_price("BTC-USD") / 5), interface.get_price("BTC-USD") / 5) - - assert limit_order.get_id() in test_utils.get_open_order_ids(interface.get_open_orders()) - limit_order_info = interface.get_order("BTC-USD", limit_order.get_id()) - assert limit_order_info["id"] == limit_order.get_id() - assert limit_order_info["status"] == "open" - assert len(interface.get_open_orders()) == initial_num_open_orders + 1 - - #cancels sell limit order - order_id = interface.cancel_order("BTC-USD", limit_order.get_id()) - assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders()) - assert len(interface.get_open_orders()) == initial_num_open_orders - - #creates sell market order - market_order = interface.market_order("BTC-USD", "sell", 5 / interface.get_price("BTC-USD")) - - while market_order.get_status() == "open": - print("Waiting for test market order to get accepted...", end = '\r') - time.sleep(1) - - print("Test market order accepted... continuing test") - - market_order_info = interface.get_order("BTC-USD", market_order.get_id()) - - assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders()) - assert market_order_info["id"] == market_order.get_id() - assert market_order_info["status"] == "closed" - - ### end testing ### - -if __name__ == "__main__": - main() \ No newline at end of file +# """ +# +# ATTENTION: +# Kraken has NO sandbox mode, these are methods specifically designed +# to test interface methods which interact with LIVE orders, and +# move REAL funds. It is not recommended that you run these tests. +# They must be executed individually and with great caution. Blankly +# is not responsible for losses incurred by improper use of this test. +# +# How do we test methods which affect live funds? +# +# There are 5 main methods that need to be tested which involve +# using live funds +# +# 1. get_open_orders() +# 2. market_order() +# 3. limit_order() +# 4. cancel_order +# +# We test these by creating a series of assertations that can only +# pass if everything works properly. +# +# """ +# +# +# import time +# import sys +# from blankly.exchanges.orders.limit_order import LimitOrder +# from blankly.utils import utils +# from pathlib import Path +# from typing import List +# +# from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth +# from blankly.exchanges.interfaces.kraken.kraken_interface import KrakenInterface +# from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory +# import test_kraken_interface_utils as test_utils +# +# +# +# def kraken_interface() -> KrakenInterface: +# keys_file_path = Path("tests/config/keys.json").resolve() +# settings_file_path = Path("tests/config/settings.json").resolve() +# +# auth_obj = KrakenAuth(str(keys_file_path), "default") +# _, kraken_interface = DirectCallsFactory.create("kraken", auth_obj, str(settings_file_path)) +# return kraken_interface +# +# """ +# cancels ALL open orders and distributes funds for testing purposes +# +# pass command line argument reset to activate +# +# use with extreme caution +# +# """ +# def reset(kraken_interface: KrakenInterface): +# validation = input("Continuing with this test will cancel ALL existing open orders and redistribute funds for testing purposes. Continue (Y/n)? ") +# if validation.lower() != "y": +# print("aborting test") +# quit() +# +# for order in kraken_interface.get_open_orders(): +# kraken_interface.cancel_order("BTC-USD", order["id"]) +# +# +# def main(): +# +# interface: KrakenInterface = kraken_interface() +# +# validation = input("To complete this test properly, you should have around $10 in USD in your Kraken account.\nThis test will involve the conversion of no more than $10 to BTC.\nDo not have any trades going on while completing this test.\nContinue (Y/n)? ") +# if validation.lower() != "y": +# quit() +# +# if len(sys.argv) == 2: +# if sys.argv[1] == "reset": +# reset(interface) +# else: +# print("Invalid command line argument. Aborting") +# +# ### start testing ### +# +# +# initial_num_open_orders = len(interface.get_open_orders()) +# +# #creates a buy limit order unlikely to be fulfilled (buying a bitcoin for $5) +# limit_order = interface.limit_order("BTC-USD", "buy", 1, 5) +# assert len(interface.get_open_orders()) == 1 + initial_num_open_orders, f"expected {1 + initial_num_open_orders} but got {len(interface.get_open_orders())}" +# assert limit_order.get_status()['status'] == "new" or limit_order.get_status()['status'] == "open", f"expected open but got {limit_order.get_status()['status']} of type {type(limit_order.get_status()['status'])}" +# +# open_order_ids: List[int] = test_utils.get_open_order_ids(interface.get_open_orders()) +# limit_order_id = int(limit_order.get_id()) +# assert limit_order_id in open_order_ids, f"id {limit_order_id} (type: {type(limit_order_id)}) not in list {open_order_ids} (type: {type(open_order_ids)})" +# +# #cancels the open limit order +# order_id = int(interface.cancel_order("BTC-USD", limit_order.get_id())) +# +# curr_num_open_orders = len(interface.get_open_orders()) +# assert curr_num_open_orders == initial_num_open_orders, f"expected {initial_num_open_orders} open orders, but got {curr_num_open_orders}\nopen order list:\n{interface.get_open_orders()}" +# assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders()) +# +# #creates a buy market order for $5 worth of bitcoin +# market_order = interface.market_order("BTC-USD", "buy", 5 / interface.get_price("BTC-USD")) +# +# while market_order.get_status() == "open": +# print("Waiting for test market order to get accepted...", end = '\r') +# time.sleep(1) +# +# print("Test market order accepted... continuing test") +# +# market_order_info = interface.get_order("BTC-USD", market_order.get_id()) +# +# assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders()) +# assert market_order_info["id"] == market_order.get_id() +# market_order_status = market_order_info["status"] +# assert market_order_status == "closed", f"expected closed, but got {market_order_status} (type: {type(market_order_status)}) \nopen orders: {interface.get_open_orders()}" +# +# #creates a sell limit order unlikely to get fulfilled (selling $5 worth of bitcoin for $5000) +# limit_order = interface.limit_order("BTC-USD", "sell", 5000 * (interface.get_price("BTC-USD") / 5), interface.get_price("BTC-USD") / 5) +# +# assert limit_order.get_id() in test_utils.get_open_order_ids(interface.get_open_orders()) +# limit_order_info = interface.get_order("BTC-USD", limit_order.get_id()) +# assert limit_order_info["id"] == limit_order.get_id() +# assert limit_order_info["status"] == "open" +# assert len(interface.get_open_orders()) == initial_num_open_orders + 1 +# +# #cancels sell limit order +# order_id = interface.cancel_order("BTC-USD", limit_order.get_id()) +# assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders()) +# assert len(interface.get_open_orders()) == initial_num_open_orders +# +# #creates sell market order +# market_order = interface.market_order("BTC-USD", "sell", 5 / interface.get_price("BTC-USD")) +# +# while market_order.get_status() == "open": +# print("Waiting for test market order to get accepted...", end = '\r') +# time.sleep(1) +# +# print("Test market order accepted... continuing test") +# +# market_order_info = interface.get_order("BTC-USD", market_order.get_id()) +# +# assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders()) +# assert market_order_info["id"] == market_order.get_id() +# assert market_order_info["status"] == "closed" +# +# ### end testing ### +# +# if __name__ == "__main__": +# main() \ No newline at end of file diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py index 2cae6b19..fa8cc29b 100644 --- a/tests/exchanges/test_interface_homogeneity.py +++ b/tests/exchanges/test_interface_homogeneity.py @@ -58,6 +58,14 @@ def setUpClass(cls) -> None: cls.interfaces.append(cls.Coinbase_Pro_Interface) cls.data_interfaces.append(cls.Coinbase_Pro_Interface) + #kraken definition and appending + cls.Kraken = blankly.Kraken(portfolio_name="default", + keys_path='./tests/config/keys.json', + settings_path="./tests/config/settings.json") + cls.Kraken_Interface = cls.Kraken.get_interface() + cls.interfaces.append(cls.Kraken_Interface) + cls.data_interfaces.append(cls.Kraken_Interface) + # Binance definition and appending cls.Binance = blankly.Binance(portfolio_name="Spot Test Key", keys_path='./tests/config/keys.json', @@ -72,13 +80,7 @@ def setUpClass(cls) -> None: cls.Binance_Interface_data = cls.Binance_data.get_interface() cls.data_interfaces.append(cls.Binance_Interface_data) - #kraken definition and appending - cls.Kraken = blankly.Kraken(portfolio_name="default", - keys_path='./tests/config/keys.json', - settings_path="./tests/config/settings.json") - cls.Kraken_Interface = cls.Kraken.get_interface() - cls.interfaces.append(cls.Kraken_Interface) - cls.data_interfaces.append(cls.Kraken_Interface) + # alpaca definition and appending cls.alpaca = blankly.Alpaca(portfolio_name="alpaca test portfolio", From d7c19cef150ca6a3a088e1d78a733d370d29f971 Mon Sep 17 00:00:00 2001 From: Avi Date: Sat, 19 Feb 2022 18:42:34 -0500 Subject: [PATCH 05/13] 9/12 kraken functions passing --- .../interfaces/kraken/kraken_interface.py | 66 ++++++++++++------- tests/exchanges/test_interface_homogeneity.py | 5 +- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 555fcbd5..bffeaa8e 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -1,18 +1,18 @@ -import time +# import time import pandas as pd from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI from typing import List import warnings -import blankly.utils.time_builder +# import blankly.utils.time_builder import blankly.utils.utils as utils import blankly.exchanges.interfaces.kraken.kraken_interface_utils as interface_utils from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface from blankly.exchanges.orders.limit_order import LimitOrder from blankly.exchanges.orders.market_order import MarketOrder -from blankly.exchanges.orders.stop_limit import StopLimit -from blankly.utils.exceptions import APIException, InvalidOrder +# from blankly.exchanges.orders.stop_limit import StopLimit +# from blankly.utils.exceptions import APIException, InvalidOrder class KrakenInterface(ExchangeInterface): @@ -74,11 +74,11 @@ def get_products(self) -> list: def get_account(self, symbol=None): symbol = super().get_account(symbol) - positions = self.calls.list_positions() + positions = self.get_open_orders() positions_dict = utils.AttributeDict({}) for position in positions: - curr_symbol = position.pop('symbol') + curr_symbol = position["result"]["open"]["desc"]["pair"] positions_dict[curr_symbol] = utils.AttributeDict({ 'available': float(position.pop('vol')), 'hold': 0.0 @@ -87,19 +87,13 @@ def get_account(self, symbol=None): symbols = list(positions_dict.keys()) # Catch an edge case bug that if there are no positions it won't try to snapshot if len(symbols) != 0: - open_orders = self.calls.list_orders(status='open', symbols=symbols) - snapshot_price = self.calls.get_snapshots(symbols=symbols) + open_orders = self.get_open_orders(symbol=symbols) + #calls.list_orders(status='open', symbols=symbols) + snapshot_price = self.get_open_orders(symbol=symbols)["result"]["open"]["desc"]["price"] else: open_orders = [] snapshot_price = {} - # now grab the available cash in the account - account = self.calls.get_account() - positions_dict['USD'] = utils.AttributeDict({ - 'available': float(account['buying_power']), - 'hold': 0.0 - }) - for order in open_orders: curr_symbol = order['symbol'] if order['side'] == 'buy': # buy orders only affect USD holds @@ -225,7 +219,7 @@ def get_open_orders(self, symbol: str = None) -> list: """ response = self.get_calls().open_orders(symbol)["open"] response_needed_fulfilled = [] - if (len(response) == 0): + if len(response) == 0: return [] for open_order_id, open_order_info in response.items(): @@ -328,10 +322,28 @@ def get_fees(self) -> dict: "taker_fee_rate": fee_taker, } - def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd.DataFrame: + def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwargs) -> pd.DataFrame: + """ + Kucoin is strange because it's exclusive instead of inclusive. This generally invovles adding an extra + datapoint so this is here to do some of that work + """ + to = kwargs['to'] + + if isinstance(to, str): + epoch_start -= resolution + elif isinstance(to, int): + epoch_start = epoch_stop - (to * resolution) + + return self.get_product_history(symbol, epoch_start, epoch_stop, resolution) + + def get_product_history(self, symbol, epoch_start, epoch_stop, resolution)-> pd.DataFrame: # symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) symbol = "XBTUSD" + + # epoch_start = int(utils.convert_epochs(epoch_start)) + # epoch_stop = int(utils.convert_epochs(epoch_stop)) + accepted_grans = [60, 300, 900, 1800, 3600, 14400, 86400, 604800, 1296000] if resolution not in accepted_grans: @@ -339,6 +351,10 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd resolution = accepted_grans[min(range(len(accepted_grans)), key=lambda i: abs(accepted_grans[i] - resolution))] + # resolution = int(resolution) + # + # epoch_start -= resolution + # kraken processes resolution in seconds, so the granularity must be divided by 60 product_history = self.get_calls().ohlc(symbol, interval=(resolution / 60), since=epoch_start) symbol = "XXBTZUSD" @@ -358,16 +374,18 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd if time > epoch_stop: break - historical_data_block.append([time, low, high, open_, close, volume]) + historical_data_block.append([time, open_, close, high, low, volume]) utils.update_progress(i / num_intervals) print("\n") - df = pd.DataFrame(historical_data_block, columns=['time', 'low', 'high', 'open', 'close', 'volume']) - df_start = df["time"][0] - if df_start > epoch_start: + df = pd.DataFrame(historical_data_block, columns=['time', 'open', 'close', 'high', 'low', 'volume']) + df[['time']] = df[['time']].astype(int) + # df_start = df["time"][0] + + if df["time"][0] > epoch_start: warnings.warn( - f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df_start}") + f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df['time'][0]}") df = df.astype({ 'open': float, @@ -376,6 +394,7 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution) -> pd 'close': float, 'volume': float }) + return df def get_order_filter(self, symbol: str) -> dict: @@ -388,7 +407,8 @@ def get_order_filter(self, symbol: str) -> dict: price = self.get_calls().ticker(kraken_symbol)[kraken_symbol]["c"][0] - # max_orders = self.account_levels_to_max_open_orders[self.user_preferences["settings"]["kraken"]["account_type"]] + # max_orders = self.account_levels_to_max_open_orders[ + # self.user_preferences["settings"]["kraken"]["account_type"]] return { "symbol": symbol, diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py index fa8cc29b..492839d1 100644 --- a/tests/exchanges/test_interface_homogeneity.py +++ b/tests/exchanges/test_interface_homogeneity.py @@ -134,6 +134,7 @@ def test_get_account(self): availability_results = [] for i in range(len(self.interfaces)): + print(i) if self.interfaces[i].get_exchange_type() == "alpaca": responses.append(self.interfaces[i].get_account()['AAPL']) responses.append(self.interfaces[i].get_account('AAPL')) @@ -416,8 +417,8 @@ def test_start_with_end_history(self): responses = [] # This initial selection could fail because of the slightly random day that they delete their data - stop_dt = dateparser.parse("2021-08-04") - start = "2021-01-07" + stop_dt = dateparser.parse("2021-11-08") + start = "2021-10-04" stop = str(stop_dt.date()) # The dates are offset by one because the time is the open time From f276716676086fc94f1f9b6f77d7b405005e4c97 Mon Sep 17 00:00:00 2001 From: Avi Date: Sun, 27 Feb 2022 21:17:39 -0500 Subject: [PATCH 06/13] added kraken websocket functionality --- .../interfaces/kraken/kraken_websocket.py | 159 +++++++++--------- .../kraken/kraken_websocket_utils.py | 52 ++++++ .../managers/general_stream_manager.py | 12 ++ .../exchanges/managers/orderbook_manager.py | 61 +++++++ blankly/exchanges/managers/ticker_manager.py | 16 ++ blankly/utils/utils.py | 2 +- 6 files changed, 221 insertions(+), 81 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_websocket.py b/blankly/exchanges/interfaces/kraken/kraken_websocket.py index 19f812ae..d59c6a9f 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_websocket.py +++ b/blankly/exchanges/interfaces/kraken/kraken_websocket.py @@ -5,38 +5,18 @@ import time import traceback +import websocket from websocket import create_connection import blankly import blankly.exchanges.interfaces.kraken.kraken_websocket_utils as websocket_utils from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket - - -def create_ticker_connection(id, url, channel): - ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE}) - request = json.dumps({ - "op": "subscribe", - "channel": channel, - "market": id - }) - ws.send(request) - return ws - - -# This could be needed: -# "channels": [ -# { -# "name": "ticker", -# "product_ids": [ -# \"""" + id + """\" -# ] -# } -# ] +from blankly.utils.utils import info_print class Tickers(ABCExchangeWebsocket): def __init__(self, symbol, stream, log=None, - pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="ws.kraken.com"): + pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="wss://ws.kraken.com"): """ Create and initialize the ticker Args: @@ -60,16 +40,18 @@ def __init__(self, symbol, stream, log=None, else: self.__log = False + self.__preferences = blankly.utils.load_user_preferences() + self.URL = WEBSOCKET_URL self.ws = None self.__response = None self.__most_recent_tick = None self.__most_recent_time = None + self.__thread = threading.Thread(target=self.read_websocket) self.__callbacks = [] self.__pre_event_callback = pre_event_callback + self.__message_count = 0 - # Reload preferences - self.__preferences = blankly.utils.load_user_preferences() buffer_size = self.__preferences["settings"]["websocket_buffer_size"] self.__ticker_feed = collections.deque(maxlen=buffer_size) self.__time_feed = collections.deque(maxlen=buffer_size) @@ -83,72 +65,89 @@ def start_websocket(self): Restart websocket if it was asked to stop. """ if self.ws is None: - self.ws = create_ticker_connection(self.__id, self.URL, self.__stream) - self.__response = self.ws.recv() - thread = threading.Thread(target=self.read_websocket) - thread.start() + self.ws = websocket.WebSocketApp(self.URL, + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + self.__thread = threading.Thread(target=self.read_websocket) + self.__thread.start() else: - if self.ws.connected: - print("Already running...") - pass + if self.__thread.is_alive(): + info_print("Already running...") else: # Use recursion to restart, continue appending to time feed and ticker feed self.ws = None self.start_websocket() def read_websocket(self): - counter = 0 - # TODO port this to "WebSocketApp" found in the websockets documentation - while self.ws.connected: - # In case the user closes while its reading from the websocket, this will let it expire - persist_connected = self.ws.connected - try: - received_string = self.ws.recv() - received_dict = json.loads(received_string) - parsed_received_trades = websocket_utils.process_trades(received_dict) - for received in parsed_received_trades: - #ISO8601 is converted to epoch in process_trades - self.__most_recent_time = received["time"] - self.__time_feed.append(self.__most_recent_time) - - if self.__log: - if counter % 100 == 0: - self.__file.close() - self.__file = open(self.__filePath, 'a') - line = self.__logging_callback(received) - self.__file.write(line) - - # Manage price events and fire for each manager attached - interface_message = self.__interface_callback(received) - self.__ticker_feed.append(interface_message) - self.__most_recent_tick = interface_message - - try: - for i in self.__callbacks: - i(interface_message) - except Exception: - traceback.print_exc() - - counter += 1 - except Exception as e: - if persist_connected: - traceback.print_exc() - pass - else: + # Main thread to sit here and run + self.ws.run_forever() + # This repeats the close behavior just in case something happens + + def on_message(self, ws, message): + + message = json.loads(message) + print(message) + if isinstance(message, dict): + if message['status'] == 'subscribed': + channel = message['channelName'] + info_print(f"Subscribed to {channel}") + return + elif message['status'] == 'heartbeat' or message['status'] == 'online': + return + else: + if message[-2] == 'trade': + self.__most_recent_time = message[1][0][2] + self.__time_feed.append(self.__most_recent_time) + #self.__log_response(self.__logging_callback, message) + + if self.__log: + if self.__message_count % 100 == 0: + self.__file.close() + self.__file = open(self.__filePath, 'a') + line = self.__logging_callback(message) + self.__file.write(line) + + # Manage price events and fire for each manager attached + interface_message = self.__interface_callback(message) + self.__ticker_feed.append(interface_message) + self.__most_recent_tick = interface_message + + try: + for i in self.__callbacks: + i(interface_message) + except Exception as e: + info_print(e) traceback.print_exc() - print("Error reading ticker websocket for " + self.__id + " on " + - self.__stream + ": attempting to re-initialize") - # Give a delay so this doesn't eat up from the main thread if it takes many tries to initialize - time.sleep(2) - self.ws.close() - self.ws = create_ticker_connection(self.__id, self.URL, self.__stream) - # Update response - self.__response = self.ws.recv() + + self.__message_count += 1 + + def on_error(self, ws, error): + print(error) + + def on_close(self, ws): + # This repeats the close behavior just in case something happens + pass + + def on_open(self, ws): + #ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE}) + request = json.dumps({ + "event": "subscribe", + "pair": [ + self.__id + ], + "subscription": { + "name": self.__stream + } + }) + ws.send(request) + return ws """ Required in manager """ def is_websocket_open(self): - return self.ws.connected + return self.__thread.is_alive() def get_currency_id(self): return self.__id @@ -191,7 +190,7 @@ def close_websocket(self): if self.ws.connected: self.ws.close() else: - print("Websocket for " + self.__id + ' on channel ' + self.__stream + " is already closed") + print("Websocket for " + self.__id + '@' + self.__stream + " is already closed") """ Required in manager """ diff --git a/blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py b/blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py index e69de29b..19a26813 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py +++ b/blankly/exchanges/interfaces/kraken/kraken_websocket_utils.py @@ -0,0 +1,52 @@ +import time + +from blankly.utils.utils import isolate_specific + + +def switch_type(stream): + if stream == "trade": + return trade, \ + trade_interface, \ + "time, system_time, price, volume, side, order_type, misc\n" + elif stream == "book": + return no_callback, \ + no_callback, \ + "" + else: + return no_callback, no_callback, "" + + +def no_callback(message): + return message + + +def trade(received): + line = str(received[1][0][2]) + "," + str(time.time()) + "," + received[1][0][0] + "," + received[1][0][1] + "," + \ + received[1][0][3] + "," + received[1][0][4] + "," + received[1][0][5] + "\n" + + return line + + + +def trade_interface(message): + symbol = message[3] + + needed = [ + ["symbol", str], + ["price", float], + ["time", float], + ["trade_id", int], + ["size", float] + ] + + output_dict = { + } + output_dict['symbol'] = symbol.replace("/", "-") + output_dict['price'] = message[1][0][0] + output_dict["time"] = message[1][0][2] + output_dict['trade_id'] = None + output_dict["size"] = message[1][0][1] + + isolated = isolate_specific(needed, output_dict) + + return isolated diff --git a/blankly/exchanges/managers/general_stream_manager.py b/blankly/exchanges/managers/general_stream_manager.py index 3be2cfee..9e7302b7 100644 --- a/blankly/exchanges/managers/general_stream_manager.py +++ b/blankly/exchanges/managers/general_stream_manager.py @@ -19,6 +19,7 @@ from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Websocket from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Websocket +from blankly.exchanges.interfaces.kraken.kraken_websocket import Tickers as Kraken_Websocket from blankly.exchanges.managers.websocket_manager import WebsocketManager @@ -82,6 +83,17 @@ def create_general_connection(self, callback, channel, log=None, override_symbol return websocket + elif exchange_cache == "kraken": + if use_sandbox: + raise ValueError("Error: Kraken does not have a sandbox mode") + else: + websocket = Kraken_Websocket(asset_id_cache, channel, log) + websocket.append_callback(callback) + + self.__websockets[channel][exchange_cache][asset_id_cache] = websocket + + return websocket + elif exchange_cache == "alpaca": stream = self.preferences['settings']['alpaca']['websocket_stream'] diff --git a/blankly/exchanges/managers/orderbook_manager.py b/blankly/exchanges/managers/orderbook_manager.py index be50adc9..55b231f8 100644 --- a/blankly/exchanges/managers/orderbook_manager.py +++ b/blankly/exchanges/managers/orderbook_manager.py @@ -26,6 +26,7 @@ from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Orderbook from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Orderbook +from blankly.exchanges.interfaces.kraken.kraken_websocket import Tickers as Kraken_Orderbook from blankly.exchanges.managers.websocket_manager import WebsocketManager @@ -150,6 +151,28 @@ def create_orderbook(self, callback, "asks": [] } return websocket + elif exchange_name == "kraken": + if override_symbol is None: + override_symbol = self.__default_currency + if use_sandbox: + raise ValueError("Error: FTX does not have a sandbox mode") + else: + websocket = Kraken_Orderbook(override_symbol, "book", + initially_stopped=initially_stopped + ) + # This is where the sorting magic happens + websocket.append_callback(self.kraken_update) + + # Store this object + self.__websockets['kraken'][override_symbol] = websocket + self.__websockets_callbacks['kraken'][override_symbol] = [callback] + self.__websockets_kwargs['kraken'][override_symbol] = kwargs + self.__orderbooks['kraken'][override_symbol] = { + "bids": [], + "asks": [] + } + return websocket + elif exchange_name == "binance": if override_symbol is None: override_symbol = self.__default_currency @@ -262,6 +285,44 @@ def coinbase_update(self, update): i(self.__orderbooks['coinbase_pro'][update['product_id']], **self.__websockets_kwargs['coinbase_pro'][update['product_id']]) + def kraken_update(self, update): + symbol = update[3].replace("/", "-") + + book_buys = self.__orderbooks['kraken'][symbol]['bids'] + book_sells = self.__orderbooks['kraken'][symbol]['asks'] + + new_buys = update[1]['bs'][::-1] # type: list + for i in new_buys: + i[0] = float(i[0]) # price + i[1] = float(i[1]) # size + if i[1] == 0: + book_buys = remove_price(book_buys, i[1]) + else: + book_buys.append((i[0], i[1])) + + # Asks are sells, these are also counted from low to high + new_sells = update[1]['as'] + for i in new_sells: + i[0] = float(i[0]) + i[1] = float(i[1]) + if i[1] == 0: + book_sells = remove_price(book_sells, i[1]) + else: + book_sells.append((i[0], i[1])) + + # Now sort them + book_buys = sort_list_tuples(book_buys) + book_sells = sort_list_tuples(book_sells) + + self.__orderbooks['kraken'][symbol]['bids'] = book_buys + self.__orderbooks['kraken'][symbol]['asks'] = book_sells + + # Pass in this new updated orderbook + callbacks = self.__websockets_callbacks['kucoin'][symbol] + for i in callbacks: + i(self.__orderbooks['kraken'][symbol], + **self.__websockets_kwargs['kraken'][symbol]) + def binance_update(self, update): try: # TODO this needs a snapshot to work correctly, which needs arun's rest code diff --git a/blankly/exchanges/managers/ticker_manager.py b/blankly/exchanges/managers/ticker_manager.py index 6699d520..8d3a5086 100644 --- a/blankly/exchanges/managers/ticker_manager.py +++ b/blankly/exchanges/managers/ticker_manager.py @@ -20,6 +20,7 @@ from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Ticker from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Ticker from blankly.exchanges.interfaces.ftx.ftx_websocket import Tickers as FTX_Ticker +from blankly.exchanges.interfaces.kraken.kraken_websocket import Tickers as Kraken_Ticker from blankly.exchanges.managers.websocket_manager import WebsocketManager @@ -39,6 +40,8 @@ def __init__(self, default_exchange: str, default_symbol: str): default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "alpaca") elif default_exchange == "ftx": default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "ftx") + elif default_exchange == "kraken": + default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "kraken") self.__default_symbol = default_symbol self.__tickers = {} @@ -138,5 +141,18 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None, self.__tickers['ftx'][override_symbol] = ticker return ticker + elif exchange_name == "kraken": + if override_symbol is None: + override_symbol = self.__default_symbol + + if sandbox_mode: + raise ValueError("Error: Kraken does not have a sandbox mode") + else: + ticker = Kraken_Ticker(override_symbol, "trade", log=log) + + ticker.append_callback(callback) + self.__tickers['kraken'][override_symbol] = ticker + return ticker + else: print(exchange_name + " ticker not supported, skipping creation") diff --git a/blankly/utils/utils.py b/blankly/utils/utils.py index 3396be02..1263df71 100644 --- a/blankly/utils/utils.py +++ b/blankly/utils/utils.py @@ -308,7 +308,7 @@ def to_exchange_symbol(blankly_symbol, exchange): return get_base_asset(blankly_symbol) if exchange == "coinbase_pro": return blankly_symbol - if exchange == 'ftx': + if exchange == 'ftx' or exchange == "kraken": return blankly_symbol.replace("-", "/") From d9a415c597886f65ec63528bb76ede6067dadb1e Mon Sep 17 00:00:00 2001 From: Emerson Dove <52636744+EmersonDove@users.noreply.github.com> Date: Wed, 30 Mar 2022 13:06:29 -0400 Subject: [PATCH 07/13] Convert kraken to new authentication scheme --- blankly/exchanges/exchange.py | 3 ++ blankly/exchanges/interfaces/kraken/kraken.py | 46 +++++++++++++++++-- .../exchanges/interfaces/kraken/kraken_api.py | 8 ++-- .../interfaces/kraken/kraken_auth.py | 8 ---- .../interfaces/kraken/kraken_interface.py | 4 +- .../kraken/kraken_interface_utils.py | 37 ++------------- 6 files changed, 54 insertions(+), 52 deletions(-) delete mode 100644 blankly/exchanges/interfaces/kraken/kraken_auth.py diff --git a/blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py index ce589fa2..c240c676 100644 --- a/blankly/exchanges/exchange.py +++ b/blankly/exchanges/exchange.py @@ -28,6 +28,7 @@ from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface from blankly.exchanges.interfaces.kucoin.kucoin_interface import KucoinInterface +from blankly.exchanges.interfaces.kraken.kraken_interface import KrakenInterface class Exchange(ABCExchange, abc.ABC): @@ -79,6 +80,8 @@ def construct_interface_and_cache(self, calls): self.interface = OandaInterface(self.__type, calls) elif self.__type == "kucoin": self.interface = KucoinInterface(self.__type, calls) + elif self.__type == "kraken": + self.interface = KrakenInterface(self.__type, calls) blankly.reporter.export_used_exchange(self.__type) diff --git a/blankly/exchanges/interfaces/kraken/kraken.py b/blankly/exchanges/interfaces/kraken/kraken.py index 40457c45..9f2a5b29 100644 --- a/blankly/exchanges/interfaces/kraken/kraken.py +++ b/blankly/exchanges/interfaces/kraken/kraken.py @@ -1,10 +1,50 @@ +""" + Kraken definition & setup + Copyright (C) 2022 Emerson Dove + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . +""" + + from blankly.exchanges.exchange import Exchange from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI +from blankly.exchanges.auth.auth_constructor import AuthConstructor +from blankly.utils import info_print +from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface +from blankly.exchanges.interfaces.kraken.kraken_interface import KrakenInterface + class Kraken(Exchange): def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None): - # Giving the preferences path as none allows us to create a default - Exchange.__init__(self, "kraken", portfolio_name, keys_path, settings_path) + Exchange.__init__(self, "kraken", portfolio_name, settings_path) + + # Load the auth from the keys file + auth = AuthConstructor(keys_path, portfolio_name, 'kraken', ['API_KEY', 'API_SECRET', 'sandbox']) + + keys = auth.keys + sandbox = super().evaluate_sandbox(auth) + + calls = KrakenAPI(keys['API_KEY'], keys['API_SECRET']) + + # Always finish the method with this function + super().construct_interface_and_cache(calls) + + # Kraken is unique because we can continue by wrapping the interface in paper trade + if sandbox: + info_print('The sandbox setting is enabled for this key. Kraken has been created as a ' + 'paper trading instance.') + self.interface = PaperTradeInterface(KrakenInterface(calls, settings_path)) """ Builds information about the asset on this exchange by making particular API calls @@ -26,4 +66,4 @@ def get_exchange_state(self): return self.interface.get_fees() def get_direct_calls(self) -> KrakenAPI: - return self.calls \ No newline at end of file + return self.calls diff --git a/blankly/exchanges/interfaces/kraken/kraken_api.py b/blankly/exchanges/interfaces/kraken/kraken_api.py index a18cb9dc..fb92d959 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_api.py +++ b/blankly/exchanges/interfaces/kraken/kraken_api.py @@ -8,18 +8,16 @@ import requests #from . import version import requests -from blankly.exchanges.auth.abc_auth import ABCAuth -from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth class API: - def __init__(self, auth: ABCAuth, API_URL='https://api.kraken.com', timeout = 30): + def __init__(self, api_key, api_secret, API_URL='https://api.kraken.com', timeout=30): self.__api_url = API_URL self.session = requests.Session() - self._api_key = auth.keys['API_SECRET'] - self._api_secret = auth.keys['API_KEY'] + self._api_key = api_key + self._api_secret = api_secret self.timeout = [timeout] self.proxy = {} self.version = "0" diff --git a/blankly/exchanges/interfaces/kraken/kraken_auth.py b/blankly/exchanges/interfaces/kraken/kraken_auth.py deleted file mode 100644 index 05536f8e..00000000 --- a/blankly/exchanges/interfaces/kraken/kraken_auth.py +++ /dev/null @@ -1,8 +0,0 @@ -from blankly.exchanges.auth.abc_auth import ABCAuth - - -class KrakenAuth(ABCAuth): - def __init__(self, keys_file, portfolio_name): - super().__init__(keys_file, portfolio_name, 'kraken') - needed_keys = ['API_KEY', 'API_SECRET'] - self.validate_credentials(needed_keys) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index bffeaa8e..15adf8ce 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -18,8 +18,8 @@ class KrakenInterface(ExchangeInterface): # NOTE, kraken has no sandbox mode - def __init__(self, authenticated_API: KrakenAPI, preferences_path: str): - super().__init__('kraken', authenticated_API, preferences_path, valid_resolutions=None) + def __init__(self, exchange_name, authenticated_api): + super().__init__(exchange_name, authenticated_api, valid_resolutions=None) self.account_levels_to_max_open_orders = { "Starter": 60, diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py b/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py index ef84d5c9..9257f8d6 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface_utils.py @@ -1,37 +1,6 @@ -import json -from pathlib import Path -from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI -from blankly.exchanges.interfaces.kraken.kraken_auth import KrakenAuth - - -def kraken_api() -> KrakenAPI: - keys_file_path = Path("./tests/config/keys.json") - - auth_obj = KrakenAuth(str(keys_file_path), "default") - api = KrakenAPI(auth_obj) - return api - - def blankly_symbol_to_kraken_symbol(blankly_symbol: str): - api = kraken_api() - response = api.asset_pairs() - - - for key, value in response.items(): - - wsname = value["wsname"].replace("/", "-") - - if wsname == blankly_symbol: - return key - - raise ValueError("Invalid blankly symbol") + return blankly_symbol.replace('-', '/') + def kraken_symbol_to_blankly_symbol(kraken_symbol: str): - api = kraken_api() - response = api.asset_pairs() - - for key, value in response.items(): - if key == kraken_symbol or value["altname"] == kraken_symbol: - return value["wsname"].replace("/", "-") - - raise ValueError("Invalid kraken symbol") \ No newline at end of file + return kraken_symbol.replace('/', '-') From cac0c4fbaaa7da7fd94c557f61c653f82b20db84 Mon Sep 17 00:00:00 2001 From: Avi Date: Mon, 11 Apr 2022 23:03:58 -0400 Subject: [PATCH 08/13] test commit on maple's branch --- blankly/exchanges/interfaces/kraken/test.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 blankly/exchanges/interfaces/kraken/test.py diff --git a/blankly/exchanges/interfaces/kraken/test.py b/blankly/exchanges/interfaces/kraken/test.py new file mode 100644 index 00000000..3066b7b5 --- /dev/null +++ b/blankly/exchanges/interfaces/kraken/test.py @@ -0,0 +1 @@ +print('works') \ No newline at end of file From 16399e49df36fb65b35f66df648eb5501c43a0c6 Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 12 Apr 2022 15:11:35 -0400 Subject: [PATCH 09/13] minor updates --- blankly/exchanges/interfaces/kraken/kraken_interface.py | 1 + tests/exchanges/test_interface_homogeneity.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 15adf8ce..14063206 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -211,6 +211,7 @@ def limit_order(self, symbol: str, side: str, price: float, size: float): def cancel_order(self, symbol: str, order_id: str): return self.get_calls().cancel_order(order_id) + #EAPI:Invalid key error for open orders def get_open_orders(self, symbol: str = None) -> list: """ List open orders. diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py index 75957591..ff218f37 100644 --- a/tests/exchanges/test_interface_homogeneity.py +++ b/tests/exchanges/test_interface_homogeneity.py @@ -403,8 +403,8 @@ def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None: cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', sorted_orders['binance']['buy'].get_id())) cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', sorted_orders['binance']['sell'].get_id())) - cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['buy'].get_id())) - cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['sell'].get_id())) + # cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['buy'].get_id())) + # cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['sell'].get_id())) cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD', sorted_orders['coinbase_pro']['buy'].get_id())) From 44ec9125e56055ca174e72384dff74dc55d781ae Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 12 Apr 2022 15:13:56 -0400 Subject: [PATCH 10/13] minor updates --- .../interfaces/kraken/kraken_interface.py | 7 ++- tests/exchanges/test_interface_homogeneity.py | 55 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 14063206..aa8809d7 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -17,7 +17,7 @@ class KrakenInterface(ExchangeInterface): - # NOTE, kraken has no sandbox mode + # NOTE: kraken has no sandbox mode def __init__(self, exchange_name, authenticated_api): super().__init__(exchange_name, authenticated_api, valid_resolutions=None) @@ -218,7 +218,8 @@ def get_open_orders(self, symbol: str = None) -> list: Args: symbol (optional) (str): Asset such as BTC-USD """ - response = self.get_calls().open_orders(symbol)["open"] + response = self.get_calls().open_orders() + response_needed_fulfilled = [] if len(response) == 0: return [] @@ -325,7 +326,7 @@ def get_fees(self) -> dict: def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwargs) -> pd.DataFrame: """ - Kucoin is strange because it's exclusive instead of inclusive. This generally invovles adding an extra + Kraken is strange because it's exclusive instead of inclusive. This generally invovles adding an extra datapoint so this is here to do some of that work """ to = kwargs['to'] diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py index ff218f37..51993388 100644 --- a/tests/exchanges/test_interface_homogeneity.py +++ b/tests/exchanges/test_interface_homogeneity.py @@ -86,20 +86,20 @@ def setUpClass(cls) -> None: cls.data_interfaces.append(cls.Coinbase_Pro_Interface) # Kucoin definition and appending - cls.Kucoin = blankly.Kucoin(portfolio_name="KC Sandbox Portfolio", - keys_path='./tests/config/keys.json', - settings_path="./tests/config/settings.json") - cls.Kucoin_Interface = cls.Kucoin.get_interface() - cls.interfaces.append(cls.Kucoin_Interface) - - cls.Kucoin_data = blankly.Kucoin(portfolio_name="KC Data Keys", - keys_path='./tests/config/keys.json', - settings_path="./tests/config/settings.json") - cls.Kucoin_Interface_data = cls.Kucoin_data.get_interface() - cls.data_interfaces.append(cls.Kucoin_Interface_data) + # cls.Kucoin = blankly.Kucoin(portfolio_name="KC Sandbox Portfolio", + # keys_path='./tests/config/keys.json', + # settings_path="./tests/config/settings.json") + # cls.Kucoin_Interface = cls.Kucoin.get_interface() + # cls.interfaces.append(cls.Kucoin_Interface) + # + # cls.Kucoin_data = blankly.Kucoin(portfolio_name="KC Data Keys", + # keys_path='./tests/config/keys.json', + # settings_path="./tests/config/settings.json") + # cls.Kucoin_Interface_data = cls.Kucoin_data.get_interface() + # cls.data_interfaces.append(cls.Kucoin_Interface_data) #kraken definition and appending - cls.Kraken = blankly.Kraken(portfolio_name="default", + cls.Kraken = blankly.Kraken(portfolio_name="kraken test portfolio", keys_path='./tests/config/keys.json', settings_path="./tests/config/settings.json") cls.Kraken_Interface = cls.Kraken.get_interface() @@ -130,12 +130,12 @@ def setUpClass(cls) -> None: cls.data_interfaces.append(cls.Alpaca_Interface) # Oanda definition and appending - cls.Oanda = blankly.Oanda(portfolio_name="oanda test portfolio", - keys_path='./tests/config/keys.json', - settings_path="./tests/config/settings.json") - cls.Oanda_Interface = cls.Oanda.get_interface() - cls.interfaces.append(cls.Oanda_Interface) - cls.data_interfaces.append(cls.Oanda_Interface) + # cls.Oanda = blankly.Oanda(portfolio_name="oanda test portfolio", + # keys_path='./tests/config/keys.json', + # settings_path="./tests/config/settings.json") + # cls.Oanda_Interface = cls.Oanda.get_interface() + # cls.interfaces.append(cls.Oanda_Interface) + # cls.data_interfaces.append(cls.Oanda_Interface) cls.FTX = blankly.FTX(portfolio_name="Main Account", keys_path='./tests/config/keys.json', @@ -348,11 +348,11 @@ def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None: limits += evaluate_limit_order(self.Binance_Interface, 'BTC-USDT', int(binance_limits['min_price']+100), int(binance_limits['max_price']-100), .01) - limits += evaluate_limit_order(self.Coinbase_Pro_Interface, 'BTC-USD', .01, 100000, 1) + # limits += evaluate_limit_order(self.Coinbase_Pro_Interface, 'BTC-USD', .01, 100000, 1) - limits += evaluate_limit_order(self.Kucoin_Interface, 'ETH-USDT', .01, 100000, 1) + # limits += evaluate_limit_order(self.Kucoin_Interface, 'ETH-USDT', .01, 100000, 1) - limits += evaluate_limit_order(self.Oanda_Interface, 'EUR-USD', .01, 100000, 1) + # limits += evaluate_limit_order(self.Oanda_Interface, 'EUR-USD', .01, 100000, 1) responses = [] status = [] @@ -360,10 +360,11 @@ def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None: open_orders = { 'coinbase_pro': self.Coinbase_Pro_Interface.get_open_orders('BTC-USD'), + 'kraken': self.Kraken_Interface.get_open_orders(), 'binance': self.Binance_Interface.get_open_orders('BTC-USDT'), - 'kucoin': self.Kucoin_Interface.get_open_orders('ETH-USDT'), + # 'kucoin': self.Kucoin_Interface.get_open_orders('ETH-USDT'), 'alpaca': self.Alpaca_Interface.get_open_orders('AAPL'), - 'oanda': self.Oanda_Interface.get_open_orders('EUR-USD') + # 'oanda': self.Oanda_Interface.get_open_orders('EUR-USD') } # Simple test to ensure that some degree of orders have been placed @@ -373,9 +374,9 @@ def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None: # Just scan through both simultaneously to reduce code copying all_orders = open_orders['coinbase_pro'] all_orders = all_orders + open_orders['binance'] - all_orders = all_orders + open_orders['kucoin'] + # all_orders = all_orders + open_orders['kucoin'] all_orders = all_orders + open_orders['alpaca'] - all_orders = all_orders + open_orders['oanda'] + # all_orders = all_orders + open_orders['oanda'] # Filter for limit orders open_orders = [] @@ -414,8 +415,8 @@ def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None: cancels.append(self.Alpaca_Interface.cancel_order('AAPL', sorted_orders['alpaca']['buy'].get_id())) cancels.append(self.Alpaca_Interface.cancel_order('AAPL', sorted_orders['alpaca']['sell'].get_id())) - cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', sorted_orders['oanda']['buy'].get_id())) - cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', sorted_orders['oanda']['sell'].get_id())) + # cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', sorted_orders['oanda']['buy'].get_id())) + # cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', sorted_orders['oanda']['sell'].get_id())) self.assertTrue(compare_responses(cancels, force_exchange_specific=False)) From 8a169ead4d79b998c95199e3258d199c638c8215 Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 20 Apr 2022 15:31:59 -0400 Subject: [PATCH 11/13] kraken additions with new api --- blankly/exchanges/interfaces/kraken/kraken.py | 14 +- .../exchanges/interfaces/kraken/kraken_api.py | 758 ------------------ .../interfaces/kraken/kraken_interface.py | 232 +++--- 3 files changed, 147 insertions(+), 857 deletions(-) delete mode 100644 blankly/exchanges/interfaces/kraken/kraken_api.py diff --git a/blankly/exchanges/interfaces/kraken/kraken.py b/blankly/exchanges/interfaces/kraken/kraken.py index 9f2a5b29..61989fd0 100644 --- a/blankly/exchanges/interfaces/kraken/kraken.py +++ b/blankly/exchanges/interfaces/kraken/kraken.py @@ -16,9 +16,7 @@ along with this program. If not, see . """ - from blankly.exchanges.exchange import Exchange -from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI from blankly.exchanges.auth.auth_constructor import AuthConstructor from blankly.utils import info_print from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface @@ -33,9 +31,17 @@ def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=Non auth = AuthConstructor(keys_path, portfolio_name, 'kraken', ['API_KEY', 'API_SECRET', 'sandbox']) keys = auth.keys + sandbox = super().evaluate_sandbox(auth) - calls = KrakenAPI(keys['API_KEY'], keys['API_SECRET']) + try: + import krakenex + except ImportError: + raise ImportError("Please \"pip install krakenex\" to use kraken with blankly.") + + calls = krakenex.API() + #calls.load_key('tests/config/keys.json') + calls.__init__(keys['API_KEY'], keys['API_SECRET']) # Always finish the method with this function super().construct_interface_and_cache(calls) @@ -65,5 +71,5 @@ def get_exchange_state(self): # TODO Populate this with useful information return self.interface.get_fees() - def get_direct_calls(self) -> KrakenAPI: + def get_direct_calls(self) -> dict: return self.calls diff --git a/blankly/exchanges/interfaces/kraken/kraken_api.py b/blankly/exchanges/interfaces/kraken/kraken_api.py deleted file mode 100644 index fb92d959..00000000 --- a/blankly/exchanges/interfaces/kraken/kraken_api.py +++ /dev/null @@ -1,758 +0,0 @@ -import hmac -import hashlib -from multiprocessing.sharedctypes import Value -import time -import urllib -import base64 -import binascii -import requests -#from . import version -import requests - - -class API: - - def __init__(self, api_key, api_secret, API_URL='https://api.kraken.com', timeout=30): - self.__api_url = API_URL - self.session = requests.Session() - - self._api_key = api_key - self._api_secret = api_secret - self.timeout = [timeout] - self.proxy = {} - self.version = "0" - - - - # Short info about API client - self.headers = { - "User-Agent": "Blankly-finance", - "Version": self.version - } - - # List of all supported Request Strings. - self.supported_requests = { - "private": [ - "Balance", "TradeBalance", "OpenOrders", "ClosedOrders", "QueryOrders", "TradesHistory", - "QueryTrades", "OpenPositions", "Ledgers", "QueryLedgers", "TradeVolume", "AddOrder", - "CancelOrder", "DepositMethods", "DepositAddress", "DepositStatus", "WithdrawInfo", "Withdraw", - "WithdrawStatus", "WithdrawCancel" - ], - "public": [ - "Time", "Assets", "AssetPairs", "Ticker", "OHLC", "Depth", "Trades", "Spread" - ] - } - - def public(self, request, parameters=None): - """ - Creates a public API call. It does not require public and private keys to be set. - :param str request: One of ``Time``, ``Assets``, ``AssetPairs``, ``Ticker``, ``OHLC``, ``Depth``, \ - ``Trades`` and ``Spread``. - :param dict parameters: Additional parameters for each request. See Kraken documentation for more information. - :returns: JSON object representing Kraken API response. - :raises: ``ValueError``, if request string is not valid. ``ConnectionError`` and \ - ``ConnectionTimeout`` on connection problem. - - """ - - parameters = parameters if parameters else {} - - # Raise error on unsupported input - if request not in self.supported_requests["public"]: - raise ValueError("Request string % is not supported by public API calls.".format(request)) - - # Path to the requested api call - path = self.__api_url + '/' + self.version + '/public/' + request - - # Connect to API server - try: - response = requests.post( - path, - data=parameters, - headers=self.headers, - timeout=self.timeout[0], - proxies=self.proxy - ) - except requests.exceptions.ConnectionError: - raise ConnectionError() - except requests.exceptions.Timeout: - raise ConnectionTimeout() - except requests.exceptions.TooManyRedirects: - raise ConnectionError() - - if response.status_code != 200: - raise ConnectionError() - else: - - err_len = response.json()["error"] - if len(err_len) > 0: - raise ValueError(f"kraken API error: {err_len}") - else: - return response.json()["result"] - - def time(self)-> dict: - """ - { - "error":[ - - ], - "result":{ - "rfc1123":"Wed, 10 May 17 10:32:24 +0000", - "unixtime":1494412344 - } - } - - """ - - return self.public("Time") - - def assets(self, assets="") -> dict: - """ - { - "result":{ - "XMLN":{ - "altname":"MLN", - "decimals":10, - "display_decimals":5, - "aclass":"currency" - }, - - ... - } - } - """ - - # According to documentation, info and aclass have only those values: - parameters = { - "info": "info", - "aclass": "currency" - } - - # Asset is not required - if len(assets) != 0: - parameters["asset"] = assets - - return self.public("Assets", parameters) - - def asset_pairs(self, info="info", pairs=""): - """ - { - "error":[ - - ], - "result":{ - "XXRPXXBT":{ - "pair_decimals":8, - "lot":"unit", - "margin_call":80, - "fees_maker":[ - [ - 0, - 0.16 - ], - ... - ], - "altname":"XRPXBT", - "quote":"XXBT", - "fees":[ - [ - 0, - 0.26 - ], - ... - ], - "aclass_quote":"currency", - "margin_stop":40, - "base":"XXRP", - "lot_multiplier":1, - "fee_volume_currency":"ZUSD", - "aclass_base":"currency", - "leverage_sell":[ - - ], - "leverage_buy":[ - - ], - "lot_decimals":8 - }, - ... - } - } - - """ - if info not in ["info", "leverage", "fees", "margin"]: - raise ValueError("Value % is not a valid info string.".format(info)) - - parameters = { - "info": info, - } - - # Pair is not required and should be omnited if empty - if len(pairs) != 0: - parameters["pair"] = pairs - - return self.public("AssetPairs", parameters) - - def ticker(self, pairs): - """ - { - "error":[ - - ], - "result":{ - "XZECZEUR":{ - "t":[ - 789, - 1563 - ], - "h":[ - "93.88889", - "93.88889" - ], - "l":[ - "83.30000", - "82.90067" - ], - "a":[ - "91.13167", - "1", - "1.000" - ], - "b":[ - "91.13166", - "1", - "1.000" - ], - "v":[ - "1409.83478992", - "3185.73131989" - ], - "p":[ - "90.08671", - "87.74001" - ], - "c":[ - "91.13068", - "3.23282073" - ], - "o":"85.87561" - } - } - } - """ - parameters = { - "pair": pairs - } - - if len(pairs) == 0: - raise ValueError("Ticker pairs parameter is required.") - - return self.public("Ticker", parameters) - - def ohlc(self, pairs, interval=1, since=0) -> dict: - """ - { - "error":[ - - ], - "result":{ - "last":1494422340, - "XZECZEUR":[ - [ - 1494379260, - "86.93479", - "86.93479", - "86.93479", - "86.93479", - "0.00000", - "0.00000000", - 0 - ], - ... - ] - } - } - """ - if interval not in [1, 5, 15, 30, 60, 240, 1440, 10080, 21600]: - raise ValueError("Interval parameter value '%' is not valid.".format(interval)) - - parameters = { - "pair": pairs, - "interval": interval - } - - if since != 0: - parameters["since"] = since - - return self.public("OHLC", parameters) - - def depth(self, pairs, count=0): - """ - { - "error":[ - - ], - "result":{ - "XZECZEUR":{ - "asks":[ - [ - "89.92088", - "34.910", - 1494422730 - ], - ... - ], - "bids":[ - [ - "88.42616", - "4.755", - 1494422729 - ], - ... - ] - } - } - } - """ - parameters = { - "pair": pairs - } - - if count != 0: - parameters["count"] = count - - return self.public("Depth", parameters) - - def trades(self, pairs, since=0): - """ - { - "error":[ - - ], - "result":{ - "last":"1494423192560021193", - "XZECZEUR":[ - [ - "86.20035", - "0.44929000", - 1494369147.0533, - "b", - "l", - "" - ], - ... - ] - } - } - """ - parameters = { - "pair": pairs - } - - if since != 0: - parameters["since"] = since - - return self.public("Trades", parameters) - - def spread(self, pairs, since=0): - """ - { - "error":[ - - ], - "result":{ - "XZECZEUR":[ - [ - 1494423011, - "88.42539", - "88.91550" - ], - ... - ], - "last":1494423011 - } - } - """ - parameters = { - "pair": pairs - } - - if since != 0: - parameters["since"] = since - - return self.public("Spread", parameters) - - def private(self, request, parameters=None): - """ - Perform a private API call. - :warning: Calling this function requires public and private keys to be set in class constructor. - :param request: One of ``Balance``, ``TradeBalance``, ``OpenOrders``, ``ClosedOrders``, ``QueryOrders``, \ - ``TradesHistory``, ``QueryTrades``, ``OpenPositions``, ``Ledgers``, ``QueryLedgers``, \ - ``TradeVolume``, ``AddOrder``, ``CancelOrder``. - :param parameters: Additional parameters for each request. See Kraken documentation for more information. - :returns: A JSON object representing Kraken API response. - :raises: ValueError is request string is not supported. ConnectionError and ConnectionTimeout on \ - connection problem. - """ - - parameters = parameters if parameters else {} - - # Check if request string is supported by this version of API - if request not in self.supported_requests["private"]: - raise ValueError("Request string '%' is not supported by private API calls".format(request)) - - path = '/' + self.version + '/private/' + request - - # Nonce parameter is required. Each call should contain nonce with greater value than the previous one. - parameters["nonce"] = int(time.time() * 1000) - - # Parse parameters to URL query string - post = urllib.parse.urlencode(parameters) - - # UTF-8 string have to be encoded first - encoded = (str(parameters['nonce']) + post).encode() - - # Message consist of path and hash of POST parameters - message = path.encode() + hashlib.sha256(encoded).digest() - - - try: - signature = hmac.new(base64.b64decode(self._api_key), message, hashlib.sha512) - except binascii.Error: - raise ValueError("Private key is not a valid base64 encoded string") - - sigdigest = base64.b64encode(signature.digest()) - - # Requred headers - headers = { - 'API-Key': self._api_secret, - 'API-Sign': sigdigest.decode() - } - headers.update(self.headers) - - # Connect to API server - try: - response = requests.post(self.__api_url + path, data=parameters, headers=headers, timeout=self.timeout[0], - proxies=self.proxy) - except requests.exceptions.ConnectionError as e: - raise ConnectionError(e.response) - except requests.exceptions.Timeout: - raise ConnectionTimeout() - except requests.exceptions.TooManyRedirects: - raise ConnectionError() - - if response.status_code != 200: - raise ConnectionError() - else: - err_len = response.json()["error"] - if len(err_len) > 0: - raise ValueError(f"kraken API error: {err_len}") - else: - return response.json()["result"] - - def balance(self): - return self.private("Balance") - - def trade_balance(self, asset="ZUSD"): - parameters = { - "aclass": "currency", - "asset": asset - } - - return self.private("TradeBalance", parameters) - - def open_orders(self, trades=False, userref=""): - """ - Gets User opened orders. See `Kraken documentation `__. - :param trades: Include trades in output? True/False. - :param userref: Restrict results to given user reference ID. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - parameters = { - "trades": "true" if trades else "false", - } - - if len(userref) != 0: - parameters["userref"] = userref - - return self.private("OpenOrders", parameters) - - def closed_orders(self, trades=False, userref="", start="", end="", offset="", closetime="both"): - """ - Returns closed orders according to parameters. See `Kraken documentation `__. - :param trades: Include trades in response? - :param userref: Restrict response to given user reference ID. - :param start: Starting timestamp or order ID. - :param end: End timestamp or order ID. - :param offset: Result offset. - :param closetime: Which time to use. One of ``open``, ``close``, ``both``. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - parameters = { - "trades": "true" if trades else "false" - } - - if len(userref) != 0: - parameters["userref"] = userref - - if len(start) != 0: - parameters["start"] = start - - if len(end) != 0: - parameters["end"] = end - - if len(offset) != 0: - parameters["offset"] = offset - - if closetime not in ["open", "close", "both"]: - raise ValueError("Parameter closetime is not valid.") - - parameters["closetime"] = closetime - - return self.private("CloseOrders", parameters) - - def query_orders(self, trades=False, userref="", txid=""): - """ - Returns orders info. See `Kraken documentation `__. - :param trades: Include trades in output? - :param userref: User reference ID. - :param txid: Comma separated list of transaction IDs. Max 2O. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - parameters = { - "trades": "true" if trades else "false" - } - - if len(userref) != 0: - parameters["userref"] = userref - - if len(txid) != 0: - if len(txid.split(",")) > 20: - raise ValueError("Parameter txid can not contain more than 20 comma separated values.") - parameters["txid"] = txid - - return self.private("QueryOrders", parameters) - - def trades_history(self, ttype="all", trades=False, start="", end="", offset=""): - """ - Gets trades history. See `Kraken documentation `__. - :param ttype: Trade type. Method checks right input - it can be one of ``all``, ``any``, ``closed``, ``no``. - :param trades: Include trades related to position in result? - :param start: Unix timestamp or trade ID. - :param end: Unix timestamp of trade ID. - :param offset: History offset. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - if type not in ["all", "any", "closed", "closing", "no"]: - raise ValueError("Type parameter is not valid.") - - parameters = { - "type": ttype, - "trades": "true" if trades else "false" - } - - if len(start) != 0: - parameters["start"] = start - - if len(end) != 0: - parameters["end"] = end - - if len(offset) != 0: - parameters["ofs"] = offset - - return self.private("TradesHistory", parameters) - - def query_trades(self, trades=False, txid=""): - """ - Returns trade info. See `Kraken documentation `__. - :param trades: Include trades related to position in response. - :param txid: Comma separated list of transaction IDs. Max 2O. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - parameters = { - "trades": "true" if trades else "false" - } - - if len(txid) != 0: - if len(txid.split(",")) > 20: - raise ValueError("Parameter txid can not contain more than 20 comma separated values.") - parameters["txid"] = txid - - return self.private("QueryTrades", parameters) - - def open_positions(self, docalcs=False, txid=""): - """ - Returns open positions list. See `Kraken documentation `__. - :param txid: Comma delimited list of transaction IDs to restrict output to - :param docalcs: Include profit/loss calculations. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - parameters = { - "docals": "true" if docalcs else "false" - } - - if len(txid) != 0: - if len(txid.split(",")) > 20: - raise ValueError("Parameter txid can not contain more than 20 comma separated values.") - parameters["txid"] = txid - - return self.private("OpenPositions", parameters) - - def ledgers(self, asset="", ltype="all", start=0, end=0, offset=0): - """ - Returns ledgers list. See `Kraken documentation `__. - :param str asset: Comma delimited list of assets to restrict output to. - :param str ltype: Type of ledger to retrieve. One of ``all``, ``deposit``, ``withdrawal`` , \ - ``trade`` and ``margin``. - :param int or str start: Starting unix timestamp or ledger ID of results. - :param int or str end: Ending unix timestamp or ledger ID of results. - :param int offset: Result offset. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - if ltype not in ["all", "deposit", "withdrawal", "trade", "margin"]: - raise ValueError("Parameter type is not valid") - - parameters = { - "type": ltype, - "aclass": "currency" - } - - if len(asset) != 0: - parameters["asset"] = asset - - if start != 0: - parameters["start"] = start - - if end != 0: - parameters["end"] = end - - if offset != 0: - parameters["offset"] = offset - - return self.private("Ledgers", parameters) - - def query_ledgers(self, lid): - """ - Returns ledger info. See `Kraken documentation `__. - :param lid: Comma delimited list of ledger ids to query info about. Max 2O. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - - parameters = {} - - if len(lid) != 0: - if len(lid.split(",")) > 20: - raise ValueError("Parameter id can not contain more than 20 comma separated values.") - parameters["id"] = lid - - return self.private("QueryLedgers", parameters) - - def trade_volume(self, pair="", fee_info=False): - """ - Gets trade volume. See `Kraken documentation `__. - :param pair: Comma delimited list of asset pairs to get fee info on. - :param fee_info: Include fee info in results? - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - parameters = {} - - if len(pair) != 0: - if len(pair.split(",")) > 20: - raise ValueError("Parameter pair can not contain more than 20 comma separated values.") - parameters["pair"] = pair - - if fee_info: - parameters["fee-info"] = "true" - - self.private("TradeVoluem", parameters) - - def add_order(self, pair, otype, ordertype, price, volume, price2=-1, leverage="none", - oflags="", starttm=0, expiretm=0, userref="", validate=False): - """ - Adds exchange order to your account. See `Kraken documentation `__. - :param pair: Asset pair. - :param otype: Type of order (buy/sell). - :param ordertype: Order type. Method tests right input to this parameter and thus may raise ``ValueError``. - Only following values are allowed - ``market``, ``stop-loss``, ``take-profit``, ``stop-loss-profit``, - ``stop-loss-profit-limit``, ``stop-loss-limit``, ``take-profit-limit``, ``trailing-stop``, ``trailing-stop-limit`` - ``stop-loss-and-limit`` and ``settle-position``. - :param price: Price, meaning depends on order type. - :param volume: Order volume in lots. - :param price2: Price, meaning depends on order type. - :param leverage: Amount of desired leverage. - :param oflags: Comma separated flags: "viqc", "fcib", "fciq", "nompp". - :param starttm: Scheduled start time. Zero (now) or unix timestamp. - :param expiretm: Expiration time. Zero (never) or unix timestamp. - :param userref: User reference id. 32-bit signed number. - :param validate: Calidate inputs only. Do not submit order. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - if type not in ["buy", "sell"]: - raise ValueError("Parameter type is not valid") - - if ordertype not in [ - "market" "stop-loss", "take-profit", "stop-loss-profit", "stop-loss-profit-limit", - "stop-loss-limit", "take-profit-limit", "trailing-stop", "trailing-stop-limit", - "stop-loss-and-limit", "settle-position" - ]: - - raise ValueError("Parameter ordertype is not valid") - - parameters = { - "pair": pair, - "type": otype, - "ordertype": ordertype, - "price": price, - "volume": volume, - "leverage": leverage, - "starttm": starttm, - "expiretm": expiretm, - } - - if price2 != -1: - parameters["price2"] = price2 - - if len(oflags) != 0: - parameters["oflags"] = oflags - - if len(userref) != 0: - parameters["userref"] = userref - - if validate: - parameters["validate"] = "true" - - return self.private("AddOrder", parameters) - - def cancel_order(self, txid): - """ - Cancels order. See `Kraken documentation `__. - :param txid: Transaction id. - :return: Response as JSON object. - :raises: Any of private method exceptions. - """ - parameters = { - "txid": txid - } - - return self.private("CancelOrder", parameters) - -class ConnectionTimeout(Exception): - - pass - \ No newline at end of file diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index aa8809d7..5acb7150 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -1,12 +1,13 @@ -# import time +import time +import requests import pandas as pd -from blankly.exchanges.interfaces.kraken.kraken_api import API as KrakenAPI from typing import List import warnings # import blankly.utils.time_builder import blankly.utils.utils as utils +from blankly.exchanges.auth.utils import load_auth import blankly.exchanges.interfaces.kraken.kraken_interface_utils as interface_utils from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface from blankly.exchanges.orders.limit_order import LimitOrder @@ -53,86 +54,97 @@ def init_exchange(self): """ + def kraken_request(self, path, data): + api_url = "https://api.kraken.com" + auth = load_auth('kraken') + headers = {} + headers['API-Key'] = auth[1]['API_KEY'] + headers['API-Sign'] = self.get_calls()._sign(data, path) + req = requests.post((api_url + path), headers=headers, data=data).json() + return req + def get_products(self) -> list: needed_asset_pairs = [] + symbol = {"pair": "XXBTZUSD"} needed = self.needed["get_products"] - asset_pairs: List[dict] = self.get_calls().asset_pairs() - for asset_id, asset_pair in asset_pairs.items(): - asset_pair["symbol"] = asset_pair["wsname"].replace('/', '-') - asset_pair["base_asset"] = asset_pair.pop("base") - asset_pair["quote_asset"] = asset_pair.pop("quote") - asset_pair["base_min_size"] = asset_pair.pop("ordermin") - asset_pair["base_max_size"] = 99999999999 - asset_pair["base_increment"] = 10 ** (-1 * float(asset_pair.pop("lot_decimals"))) - asset_pair["kraken_id"] = asset_id - needed_asset_pairs.append(utils.isolate_specific(needed, asset_pair)) + asset_pairs = self.get_calls().query_public('AssetPairs', symbol) + + asset_pairs["symbol"] = asset_pairs['result'][symbol['pair']]["wsname"].replace('/', '-') + asset_pairs["base_asset"] = asset_pairs['result'][symbol['pair']]["base"] + asset_pairs["quote_asset"] = asset_pairs['result'][symbol['pair']]["quote"] + asset_pairs["base_min_size"] = asset_pairs['result'][symbol['pair']]["ordermin"] + asset_pairs["base_max_size"] = 99999999999 + asset_pairs["base_increment"] = 10 ** (-1 * float(asset_pairs['result'][symbol['pair']]["lot_decimals"])) + asset_pairs["kraken_id"] = None + needed_asset_pairs.append(utils.isolate_specific(needed, asset_pairs)) return needed_asset_pairs - # NOTE: implement this def get_account(self, symbol=None): symbol = super().get_account(symbol) - positions = self.get_open_orders() + positions = self.kraken_request('/0/private/Balance', { + "nonce": str(int(1000 * time.time())) + }) + positions_dict = utils.AttributeDict({}) - for position in positions: - curr_symbol = position["result"]["open"]["desc"]["pair"] - positions_dict[curr_symbol] = utils.AttributeDict({ - 'available': float(position.pop('vol')), + for position in positions['result']: + positions_dict[position] = utils.AttributeDict({ + 'available': float(positions['result'][position]), 'hold': 0.0 }) - symbols = list(positions_dict.keys()) + # symbols = list(positions_dict.keys()) # Catch an edge case bug that if there are no positions it won't try to snapshot - if len(symbols) != 0: - open_orders = self.get_open_orders(symbol=symbols) - #calls.list_orders(status='open', symbols=symbols) - snapshot_price = self.get_open_orders(symbol=symbols)["result"]["open"]["desc"]["price"] - else: - open_orders = [] - snapshot_price = {} - - for order in open_orders: - curr_symbol = order['symbol'] - if order['side'] == 'buy': # buy orders only affect USD holds - if order['qty']: # this case handles qty market buy and limit buy - if order['type'] == 'limit': - dollar_amt = float(order['qty']) * float(order['limit_price']) - elif order['type'] == 'market': - dollar_amt = float(order['qty']) * snapshot_price[curr_symbol]['latestTrade']['p'] - else: # we don't have support for stop_order, stop_limit_order - dollar_amt = 0.0 - else: # this is the case for notional market buy - dollar_amt = float(order['notional']) - - # In this case we don't have to subtract because the buying power is the available money already - # we just need to add to figure out how much is actually on limits - # positions_dict['USD']['available'] -= dollar_amt - - # So just add to our hold - positions_dict['USD']['hold'] += dollar_amt - - else: - if order['qty']: # this case handles qty market sell and limit sell - qty = float(order['qty']) - else: # this is the case for notional market sell, calculate the qty with cash/price - qty = float(order['notional']) / snapshot_price[curr_symbol]['latestTrade']['p'] - - positions_dict[curr_symbol]['available'] -= qty - positions_dict[curr_symbol]['hold'] += qty - - # Note that now __unique assets could be uninitialized: - if self.__unique_assets is None: - self.init_exchange() - - for i in self.__unique_assets: - if i not in positions_dict: - positions_dict[i] = utils.AttributeDict({ - 'available': 0.0, - 'hold': 0.0 - }) + # if len(symbols) != 0: + # open_orders = self.get_open_orders(symbol=symbols) + # #calls.list_orders(status='open', symbols=symbols) + # snapshot_price = self.get_open_orders(symbol=symbols)["result"]["open"]["desc"]["price"] + # else: + # open_orders = [] + # snapshot_price = {} + # + # for order in open_orders: + # curr_symbol = order['symbol'] + # if order['side'] == 'buy': # buy orders only affect USD holds + # if order['qty']: # this case handles qty market buy and limit buy + # if order['type'] == 'limit': + # dollar_amt = float(order['qty']) * float(order['limit_price']) + # elif order['type'] == 'market': + # dollar_amt = float(order['qty']) * snapshot_price[curr_symbol]['latestTrade']['p'] + # else: # we don't have support for stop_order, stop_limit_order + # dollar_amt = 0.0 + # else: # this is the case for notional market buy + # dollar_amt = float(order['notional']) + # + # # In this case we don't have to subtract because the buying power is the available money already + # # we just need to add to figure out how much is actually on limits + # # positions_dict['USD']['available'] -= dollar_amt + # + # # So just add to our hold + # positions_dict['USD']['hold'] += dollar_amt + # + # else: + # if order['qty']: # this case handles qty market sell and limit sell + # qty = float(order['qty']) + # else: # this is the case for notional market sell, calculate the qty with cash/price + # qty = float(order['notional']) / snapshot_price[curr_symbol]['latestTrade']['p'] + # + # positions_dict[curr_symbol]['available'] -= qty + # positions_dict[curr_symbol]['hold'] += qty + # + # # Note that now __unique assets could be uninitialized: + # if self.__unique_assets is None: + # self.init_exchange() + # + # for i in self.__unique_assets: + # if i not in positions_dict: + # positions_dict[i] = utils.AttributeDict({ + # 'available': 0.0, + # 'hold': 0.0 + # }) if symbol is not None: if symbol in positions_dict: @@ -164,18 +176,29 @@ def market_order(self, symbol: str, side: str, size: float): ["side", str] ], """ - symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - response = self.get_calls().add_order(symbol, side, "market", 0, size) + #symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) + symbol = 'XBTUSD' + response = self.kraken_request('/0/private/AddOrder', { + "nonce": str(int(1000*time.time())), + "ordertype": "market", + "type": side, + "volume": size, + "pair": symbol, + }) txid = response["txid"] - order_info = self.get_calls().query_orders(txid=txid) + order_info = self.kraken_request('/0/private/QueryOrders', { + "nonce": str(int(1000*time.time())), + "txid": txid, + "trades": True #default is false, so dk bout this + }) order_info["symbol"] = symbol order_info["id"] = txid - order_info["created_at"] = order_info.pop("opentm") + order_info["created_at"] = order_info['result'][txid]['opentim'] order_info["size"] = size order_info["type"] = "market" - order_info["side"] = order_info["descr"]["type"] + order_info["side"] = side order = { 'size': size, @@ -186,18 +209,31 @@ def market_order(self, symbol: str, side: str, size: float): return MarketOrder(order, order_info, self) + #private function def limit_order(self, symbol: str, side: str, price: float, size: float): - symbol = interface_utils(symbol) - response = self.get_calls().add_order(symbol, side, "market", price, size) + #symbol = interface_utils(symbol) + response = self.kraken_request('/0/private/AddOrder', { + "nonce": str(int(1000*time.time())), + "ordertype": "limit", + "type": side, + "volume": size, + "pair": symbol, + "price": price + }) txid = response["txid"] - order_info = self.get_calls().query_orders(txid=txid) + + order_info = self.kraken_request('/0/private/QueryOrders', { + "nonce": str(int(1000*time.time())), + "txid": txid, + "trades": True #default is false, so dk bout this + }) order_info["symbol"] = symbol order_info["id"] = txid - order_info["created_at"] = order_info.pop("opentm") + order_info["created_at"] = order_info['result'][txid]['opentim'] order_info["size"] = size order_info["type"] = "market" - order_info["side"] = order_info["descr"]["type"] + order_info["side"] = side order = { 'size': size, @@ -209,9 +245,12 @@ def limit_order(self, symbol: str, side: str, price: float, size: float): return LimitOrder(order, order_info, self) def cancel_order(self, symbol: str, order_id: str): - return self.get_calls().cancel_order(order_id) + return self.kraken_request('/0/private/CancelOrder', { + "nonce": str(int(1000*time.time())), + "txid": order_id + }) - #EAPI:Invalid key error for open orders + #private function def get_open_orders(self, symbol: str = None) -> list: """ List open orders. @@ -270,6 +309,8 @@ def get_open_orders(self, symbol: str = None) -> list: # ["funds", float] # ], + #https://docs.kraken.com/rest/#operation/getOrdersInfo + #private function def get_order(self, symbol: str, order_id: str) -> dict: """ Get a certain order @@ -301,12 +342,12 @@ def get_order(self, symbol: str, order_id: str) -> dict: # NOTE fees are dependent on asset pair, so this is a required parameter to call the function def get_fees(self) -> dict: - asset_symbol = "XBTUSDT" - size = 1 - asset_pair_info = self.get_calls().asset_pairs() + asset_symbol = {"pair": "XBTUSDT"} + assets_pair_info = self.get_calls().query_public('AssetPairs', asset_symbol) + asset_pair_info = assets_pair_info['result'][asset_symbol['pair']] - fees_maker: List[List[float]] = asset_pair_info[(asset_symbol)]["fees_maker"] - fees_taker: List[List[float]] = asset_pair_info[(asset_symbol)]["fees"] + fees_maker: List[List[float]] = asset_pair_info["fees_maker"] + fees_taker: List[List[float]] = asset_pair_info["fees"] # fees_maker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees_maker"] # fees_taker: List[List[float]] = asset_pair_info[interface_utils.blankly_symbol_to_kraken_symbol(asset_symbol)]["fees"] @@ -341,7 +382,6 @@ def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwar def get_product_history(self, symbol, epoch_start, epoch_stop, resolution)-> pd.DataFrame: # symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - symbol = "XBTUSD" # epoch_start = int(utils.convert_epochs(epoch_start)) # epoch_stop = int(utils.convert_epochs(epoch_stop)) @@ -353,14 +393,16 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution)-> pd. resolution = accepted_grans[min(range(len(accepted_grans)), key=lambda i: abs(accepted_grans[i] - resolution))] + param_dict = {"pair": "XBTUSDT", "interval": resolution/60, "since":epoch_start} + # resolution = int(resolution) # # epoch_start -= resolution # kraken processes resolution in seconds, so the granularity must be divided by 60 - product_history = self.get_calls().ohlc(symbol, interval=(resolution / 60), since=epoch_start) - symbol = "XXBTZUSD" - historical_data_raw = product_history[symbol] + product_history = self.get_calls().query_public('OHLC', param_dict) + #symbol = "XXBTZUSD" + historical_data_raw = product_history['result'][param_dict['pair']] historical_data_block = [] num_intervals = len(historical_data_raw) @@ -378,14 +420,13 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution)-> pd. historical_data_block.append([time, open_, close, high, low, volume]) - utils.update_progress(i / num_intervals) + utils.update_progress(i / num_intervals) - print("\n") df = pd.DataFrame(historical_data_block, columns=['time', 'open', 'close', 'high', 'low', 'volume']) df[['time']] = df[['time']].astype(int) # df_start = df["time"][0] - if df["time"][0] > epoch_start: + if df["time"][0] > epoch_start or df is None: #error here. fix this warnings.warn( f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df['time'][0]}") @@ -451,13 +492,14 @@ def get_price(self, symbol: str) -> float: Args: symbol: The asset such as (BTC-USD, or MSFT) """ - symbol = "XBTUSDT" + symbol = {"pair": "XBTUSD"} #needs symbol + response_symbol = 'XXBTZUSD' # symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - resp = self.get_calls().ticker(symbol) + response = self.get_calls().query_public('Ticker', symbol) - opening_price = float(resp[symbol]["o"]) - volume_weighted_average_price = float(resp[symbol]["p"][0]) - last_price = float(resp[symbol]["c"][0]) + opening_price = float(response['result'][response_symbol]["o"]) + volume_weighted_average_price = float(response[response_symbol]["p"][0]) + last_price = float(response[response_symbol]["c"][0]) return volume_weighted_average_price From 597536122ab89f9c8fa5f29fe6ff9746e7bf8bea Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 20 Apr 2022 17:43:57 -0400 Subject: [PATCH 12/13] kraken additions with new api --- .../interfaces/kraken/kraken_interface.py | 82 +++++++------------ 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 5acb7150..36e2c306 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -209,7 +209,6 @@ def market_order(self, symbol: str, side: str, size: float): return MarketOrder(order, order_info, self) - #private function def limit_order(self, symbol: str, side: str, price: float, size: float): #symbol = interface_utils(symbol) response = self.kraken_request('/0/private/AddOrder', { @@ -250,64 +249,40 @@ def cancel_order(self, symbol: str, order_id: str): "txid": order_id }) - #private function def get_open_orders(self, symbol: str = None) -> list: """ List open orders. Args: symbol (optional) (str): Asset such as BTC-USD """ - response = self.get_calls().open_orders() + response = self.kraken_request('/0/private/OpenOrders', { + "nonce": str(int(1000*time.time())), + "trades": True #default is false, dk bout this + }) response_needed_fulfilled = [] if len(response) == 0: return [] - for open_order_id, open_order_info in response.items(): - utils.pretty_print_JSON(f"json: {response}", actually_print=True) - - # Needed: - # 'get_open_orders': [ # Key specificity changes based on order type - # ["id", str], - # ["price", float], - # ["size", float], - # ["type", str], - # ["side", str], - # ["status", str], - # ["product_id", str] - # ], - - open_order_info['id'] = open_order_id - open_order_info["size"] = open_order_info.pop("vol") - open_order_info["type"] = open_order_info["descr"].pop("ordertype") - open_order_info["side"] = open_order_info["descr"].pop("type") - open_order_info['product_id'] = interface_utils.kraken_symbol_to_blankly_symbol( - open_order_info["descr"].pop('pair')) - open_order_info['created_at'] = open_order_info.pop('opentm') - - if open_order_info["type"] == "limit": - needed = self.choose_order_specificity("limit") - open_order_info['time_in_force'] = "GTC" - open_order_info['price'] = float(open_order_info["descr"].pop("price")) - elif open_order_info["type"] == "market": - needed = self.choose_order_specificity("market") - - open_order_info = utils.isolate_specific(needed, open_order_info) - - response_needed_fulfilled.append(open_order_info) + for open_order_id in response['result']['open']: + for open_order_info in response['result']['open'][open_order_id]: + needed = self.choose_order_specificity(open_order_info['type']) - return response_needed_fulfilled + open_order_info['id'] = open_order_info['trades'][0] + open_order_info["symbol"] = open_order_info["descr"]["pair"] + open_order_info["size"] = open_order_info['vol'] + open_order_info["type"] = open_order_info["descr"]["ordertype"] + open_order_info["side"] = open_order_info["descr"]["type"] + open_order_info["status"] = None #no status given + open_order_info["price"] = open_order_info["descr"]["price"] + open_order_info["time_in_force"] = "GTC" + open_order_info['created_at'] = open_order_info['opentm'] + + open_order_info = utils.isolate_specific(needed, open_order_info) - # 'get_order': [ - # ["product_id", str], - # ["id", str], - # ["price", float], - # ["size", float], - # ["type", str], - # ["side", str], - # ["status", str], - # ["funds", float] - # ], + response_needed_fulfilled.append(open_order_info) + + return response_needed_fulfilled #https://docs.kraken.com/rest/#operation/getOrdersInfo #private function @@ -318,7 +293,11 @@ def get_order(self, symbol: str, order_id: str) -> dict: symbol: Asset that the order is under order_id: The unique ID of the order. """ - response = self.get_calls().query_orders(txid=order_id)[order_id] + response = self.kraken_request('/0/private/QueryOrders', { + "nonce": str(int(1000*time.time())), + "txid": order_id, + "trades": True + }) utils.pretty_print_JSON(response, actually_print=True) order_type = response["descr"].pop("ordertype") @@ -442,16 +421,13 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution)-> pd. def get_order_filter(self, symbol: str) -> dict: - symbol = "XBTUSDT" + symbol = {"pair": "XBTUSDT"} kraken_symbol = symbol # kraken_symbol = interface_utils.blankly_symbol_to_kraken_symbol(symbol) - asset_info = self.get_calls().asset_pairs(pairs=kraken_symbol) - - price = self.get_calls().ticker(kraken_symbol)[kraken_symbol]["c"][0] + asset_info = self.get_calls().query_public('AssetPairs', kraken_symbol) - # max_orders = self.account_levels_to_max_open_orders[ - # self.user_preferences["settings"]["kraken"]["account_type"]] + price = self.get_calls().query_public('Ticker', kraken_symbol)["result"][symbol['pair']]['p'][0] return { "symbol": symbol, From d387961245fb42d3dc6d69451e855da0e0064002 Mon Sep 17 00:00:00 2001 From: Avi Date: Mon, 25 Apr 2022 12:50:07 -0400 Subject: [PATCH 13/13] all functions working except placing orders --- .../interfaces/kraken/kraken_interface.py | 122 +++++------------- 1 file changed, 29 insertions(+), 93 deletions(-) diff --git a/blankly/exchanges/interfaces/kraken/kraken_interface.py b/blankly/exchanges/interfaces/kraken/kraken_interface.py index 36e2c306..fde50102 100644 --- a/blankly/exchanges/interfaces/kraken/kraken_interface.py +++ b/blankly/exchanges/interfaces/kraken/kraken_interface.py @@ -30,29 +30,15 @@ def __init__(self, exchange_name, authenticated_api): } def init_exchange(self): - pass - - """ - 'get_products': [ - ["symbol", str], - ["base_asset", str], - ["quote_asset", str], - ["base_min_size", float], - ["base_max_size", float], - ["base_increment", float] - ], - """ - - """ - - NOTE: - This method might behave incorrectly as it sets the symbol as - wsname (returned by Kraken API). This was chosen as it most - closely fits in with the symbol names used by the rest of the - package, but may be less stable as it may or may not be - intended to be fully relied upon. - - """ + try: + position = self.kraken_request('/0/private/Balance', { + "nonce": str(int(1000 * time.time())) + }) + except Exception as e: + if position['error'] == 'EAPI:Invalid key': + raise KeyError('Wrong Keys') + else: + raise e def kraken_request(self, path, data): api_url = "https://api.kraken.com" @@ -82,7 +68,7 @@ def get_products(self) -> list: return needed_asset_pairs def get_account(self, symbol=None): - symbol = super().get_account(symbol) + symbol = 'XXBT' positions = self.kraken_request('/0/private/Balance', { "nonce": str(int(1000 * time.time())) @@ -96,56 +82,6 @@ def get_account(self, symbol=None): 'hold': 0.0 }) - # symbols = list(positions_dict.keys()) - # Catch an edge case bug that if there are no positions it won't try to snapshot - # if len(symbols) != 0: - # open_orders = self.get_open_orders(symbol=symbols) - # #calls.list_orders(status='open', symbols=symbols) - # snapshot_price = self.get_open_orders(symbol=symbols)["result"]["open"]["desc"]["price"] - # else: - # open_orders = [] - # snapshot_price = {} - # - # for order in open_orders: - # curr_symbol = order['symbol'] - # if order['side'] == 'buy': # buy orders only affect USD holds - # if order['qty']: # this case handles qty market buy and limit buy - # if order['type'] == 'limit': - # dollar_amt = float(order['qty']) * float(order['limit_price']) - # elif order['type'] == 'market': - # dollar_amt = float(order['qty']) * snapshot_price[curr_symbol]['latestTrade']['p'] - # else: # we don't have support for stop_order, stop_limit_order - # dollar_amt = 0.0 - # else: # this is the case for notional market buy - # dollar_amt = float(order['notional']) - # - # # In this case we don't have to subtract because the buying power is the available money already - # # we just need to add to figure out how much is actually on limits - # # positions_dict['USD']['available'] -= dollar_amt - # - # # So just add to our hold - # positions_dict['USD']['hold'] += dollar_amt - # - # else: - # if order['qty']: # this case handles qty market sell and limit sell - # qty = float(order['qty']) - # else: # this is the case for notional market sell, calculate the qty with cash/price - # qty = float(order['notional']) / snapshot_price[curr_symbol]['latestTrade']['p'] - # - # positions_dict[curr_symbol]['available'] -= qty - # positions_dict[curr_symbol]['hold'] += qty - # - # # Note that now __unique assets could be uninitialized: - # if self.__unique_assets is None: - # self.init_exchange() - # - # for i in self.__unique_assets: - # if i not in positions_dict: - # positions_dict[i] = utils.AttributeDict({ - # 'available': 0.0, - # 'hold': 0.0 - # }) - if symbol is not None: if symbol in positions_dict: return utils.AttributeDict({ @@ -320,8 +256,9 @@ def get_order(self, symbol: str, order_id: str) -> dict: return response # NOTE fees are dependent on asset pair, so this is a required parameter to call the function - def get_fees(self) -> dict: + def get_fees(self, symbol) -> dict: asset_symbol = {"pair": "XBTUSDT"} + size = 1 assets_pair_info = self.get_calls().query_public('AssetPairs', asset_symbol) asset_pair_info = assets_pair_info['result'][asset_symbol['pair']] @@ -394,20 +331,21 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution)-> pd. close = interval[4] volume = interval[5] + historical_data_block.append([time, open_, close, high, low, volume]) + if time > epoch_stop: break - historical_data_block.append([time, open_, close, high, low, volume]) - utils.update_progress(i / num_intervals) df = pd.DataFrame(historical_data_block, columns=['time', 'open', 'close', 'high', 'low', 'volume']) df[['time']] = df[['time']].astype(int) # df_start = df["time"][0] - if df["time"][0] > epoch_start or df is None: #error here. fix this - warnings.warn( - f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df['time'][0]}") + if df is not None: + if df["time"][0] > epoch_start: + warnings.warn( + f"Due to kraken's API limitations, we could only collect OHLC data as far back as unix epoch {df['time'][0]}") df = df.astype({ 'open': float, @@ -430,24 +368,24 @@ def get_order_filter(self, symbol: str) -> dict: price = self.get_calls().query_public('Ticker', kraken_symbol)["result"][symbol['pair']]['p'][0] return { - "symbol": symbol, - "base_asset": asset_info[kraken_symbol]["wsname"].split("/")[0], - "quote_asset": asset_info[kraken_symbol]["wsname"].split("/")[1], + "symbol": symbol['pair'], + "base_asset": asset_info['result'][kraken_symbol['pair']]["wsname"].split("/")[0], + "quote_asset": asset_info['result'][kraken_symbol['pair']]["wsname"].split("/")[1], "max_orders": self.account_levels_to_max_open_orders[ self.user_preferences["settings"]["kraken"]["account_type"]], "limit_order": { - "base_min_size": asset_info[kraken_symbol]["ordermin"], + "base_min_size": asset_info['result'][kraken_symbol['pair']]["ordermin"], "base_max_size": 999999999, - "base_increment": asset_info[kraken_symbol]["lot_decimals"], - "price_increment": asset_info[kraken_symbol]["pair_decimals"], - "min_price": float(asset_info[kraken_symbol]["ordermin"]) * float(price), + "base_increment": asset_info['result'][kraken_symbol['pair']]["lot_decimals"], + "price_increment": asset_info['result'][kraken_symbol['pair']]["pair_decimals"], + "min_price": float(asset_info['result'][kraken_symbol['pair']]["ordermin"]) * float(price), "max_price": 999999999, }, "market_order": { - "base_min_size": asset_info[kraken_symbol]["ordermin"], + "base_min_size": asset_info['result'][kraken_symbol['pair']]["ordermin"], "base_max_size": 999999999, - "base_increment": asset_info[kraken_symbol]["lot_decimals"], - "quote_increment": asset_info[kraken_symbol]["pair_decimals"], + "base_increment": asset_info['result'][kraken_symbol['pair']]["lot_decimals"], + "quote_increment": asset_info['result'][kraken_symbol['pair']]["pair_decimals"], "buy": { "min_funds": 0, "max_funds": 999999999, @@ -474,8 +412,6 @@ def get_price(self, symbol: str) -> float: response = self.get_calls().query_public('Ticker', symbol) - opening_price = float(response['result'][response_symbol]["o"]) - volume_weighted_average_price = float(response[response_symbol]["p"][0]) - last_price = float(response[response_symbol]["c"][0]) + volume_weighted_average_price = float(response['result'][response_symbol]["p"][0]) return volume_weighted_average_price