From db78dbf276814069fda5ad3204ee1260ad236fce Mon Sep 17 00:00:00 2001 From: nardew <28791551+nardew@users.noreply.github.com> Date: Sun, 19 Sep 2021 14:59:03 +0200 Subject: [PATCH] Coinmate (#73) --- CHANGELOG.md | 9 +- README.md | 4 +- cryptoxlib/CryptoXLib.py | 7 +- cryptoxlib/CryptoXLibClient.py | 4 +- cryptoxlib/clients/coinmate/CoinmateClient.py | 268 ++++++++++++++++++ .../clients/coinmate/CoinmateWebsocket.py | 193 +++++++++++++ cryptoxlib/clients/coinmate/__init__.py | 0 cryptoxlib/clients/coinmate/enums.py | 16 ++ cryptoxlib/clients/coinmate/exceptions.py | 15 + cryptoxlib/clients/coinmate/functions.py | 5 + examples/coinmate_rest_api.py | 39 +++ examples/coinmate_ws.py | 54 ++++ setup.py | 3 +- tests/e2e/coinmate.py | 192 +++++++++++++ 14 files changed, 804 insertions(+), 5 deletions(-) create mode 100644 cryptoxlib/clients/coinmate/CoinmateClient.py create mode 100644 cryptoxlib/clients/coinmate/CoinmateWebsocket.py create mode 100644 cryptoxlib/clients/coinmate/__init__.py create mode 100644 cryptoxlib/clients/coinmate/enums.py create mode 100644 cryptoxlib/clients/coinmate/exceptions.py create mode 100644 cryptoxlib/clients/coinmate/functions.py create mode 100644 examples/coinmate_rest_api.py create mode 100644 examples/coinmate_ws.py create mode 100644 tests/e2e/coinmate.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb189c..25531b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html ## [Pending release] +## [5.2.0] - 2021-09-19 + +### Added + +- `coinmate` cryptoexchange added! + ## [5.1.6] - 2021-08-23 ### Fixed @@ -198,7 +204,8 @@ bitvavo.enums.CandelstickInterval -> bitvavo.enums.CandlestickInterval The official release of `cryptoxlib-aio`. -[Pending release]: https://github.com/nardew/cryptoxlib-aio/compare/5.1.6...HEAD +[Pending release]: https://github.com/nardew/cryptoxlib-aio/compare/5.2.0...HEAD +[5.2.0]: https://github.com/nardew/cryptoxlib-aio/compare/5.1.6...5.2.0 [5.1.6]: https://github.com/nardew/cryptoxlib-aio/compare/5.1.5...5.1.6 [5.1.5]: https://github.com/nardew/cryptoxlib-aio/compare/5.1.4...5.1.5 [5.1.4]: https://github.com/nardew/cryptoxlib-aio/compare/5.1.3...5.1.4 diff --git a/README.md b/README.md index d3d77bc..ce3b3eb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# cryptoxlib-aio 5.1.6 +# cryptoxlib-aio 5.2.0 ![](https://img.shields.io/badge/python-3.6-blue.svg) ![](https://img.shields.io/badge/python-3.7-blue.svg) ![](https://img.shields.io/badge/python-3.8-blue.svg) ![](https://img.shields.io/badge/python-3.9-blue.svg) @@ -8,6 +8,7 @@ ### What's been recently added +- `coinmate` cryptoexchange added! - `bitpanda` cancellation of all orders via websockets - `binance` BSwap (liquidity pools) endpoints - `binance` leveraged token endpoints @@ -54,6 +55,7 @@ As mentioned earlier, all exchanges listed below include full support for websoc | ![bitpanda](https://raw.githubusercontent.com/nardew/cryptoxlib-aio/master/images/bitpanda.png) | Bitpanda Pro | [API](https://developers.bitpanda.com/exchange/) | | ![bitvavo](https://raw.githubusercontent.com/nardew/cryptoxlib-aio/master/images/bitvavo.png) | Bitvavo | [API](https://docs.bitvavo.com/#section/Introduction) | | ![btse](https://raw.githubusercontent.com/nardew/cryptoxlib-aio/master/images/btse.png) | BTSE | [API](https://www.btse.com/apiexplorer/spot/#btse-spot-api) | +| ![coinmate](https://user-images.githubusercontent.com/51840849/87460806-1c9f3f00-c616-11ea-8c46-a77018a8f3f4.jpg) | Coinmate | [API](https://coinmate.docs.apiary.io/) | | ![eterbase](https://user-images.githubusercontent.com/1294454/82067900-faeb0f80-96d9-11ea-9f22-0071cfcb9871.jpg) | Eterbase | [API](https://developers.eterbase.exchange) | | ![hitbtc](https://user-images.githubusercontent.com/1294454/27766555-8eaec20e-5edc-11e7-9c5b-6dc69fc42f5e.jpg) | HitBTC | [API](https://api.hitbtc.com) | | ![liquid](https://user-images.githubusercontent.com/1294454/45798859-1a872600-bcb4-11e8-8746-69291ce87b04.jpg) | Liquid | [API](https://developers.liquid.com) | diff --git a/cryptoxlib/CryptoXLib.py b/cryptoxlib/CryptoXLib.py index 0edb798..6a93436 100644 --- a/cryptoxlib/CryptoXLib.py +++ b/cryptoxlib/CryptoXLib.py @@ -12,6 +12,7 @@ from cryptoxlib.clients.aax.AAXClient import AAXClient from cryptoxlib.clients.hitbtc.HitbtcClient import HitbtcClient from cryptoxlib.clients.eterbase.EterbaseClient import EterbaseClient +from cryptoxlib.clients.coinmate.CoinmateClient import CoinmateClient class CryptoXLib(object): @@ -78,4 +79,8 @@ def create_hitbtc_client(api_key: str, sec_key: str) -> HitbtcClient: @staticmethod def create_eterbase_client(account_id: str, api_key: str, sec_key: str) -> EterbaseClient: - return EterbaseClient(account_id, api_key, sec_key) \ No newline at end of file + return EterbaseClient(account_id, api_key, sec_key) + + @staticmethod + def create_coinmate_client(user_id: str, api_key: str, sec_key: str) -> CoinmateClient: + return CoinmateClient(user_id, api_key, sec_key) \ No newline at end of file diff --git a/cryptoxlib/CryptoXLibClient.py b/cryptoxlib/CryptoXLibClient.py index 9cb7410..20af780 100644 --- a/cryptoxlib/CryptoXLibClient.py +++ b/cryptoxlib/CryptoXLibClient.py @@ -96,9 +96,11 @@ async def _create_put(self, resource: str, data: dict = None, params: dict = Non async def _create_rest_call(self, rest_call_type: RestCallType, resource: str, data: dict = None, params: dict = None, headers: dict = None, signed: bool = False, api_variable_path: str = None) -> dict: with Timer('RestCall'): - # ensure headers is always a valid object + # ensure headers & params are always valid objects if headers is None: headers = {} + if params is None: + params = {} # add signature into the parameters if signed: diff --git a/cryptoxlib/clients/coinmate/CoinmateClient.py b/cryptoxlib/clients/coinmate/CoinmateClient.py new file mode 100644 index 0000000..757309a --- /dev/null +++ b/cryptoxlib/clients/coinmate/CoinmateClient.py @@ -0,0 +1,268 @@ +import ssl +import logging +import datetime +import hmac +import hashlib +import pytz +from multidict import CIMultiDictProxy +from typing import List, Optional + +from cryptoxlib.CryptoXLibClient import CryptoXLibClient, RestCallType +from cryptoxlib.clients.coinmate.functions import map_pair +from cryptoxlib.clients.coinmate.exceptions import CoinmateRestException, CoinmateException +from cryptoxlib.clients.coinmate import enums +from cryptoxlib.clients.coinmate.CoinmateWebsocket import CoinmateWebsocket +from cryptoxlib.Pair import Pair +from cryptoxlib.WebsocketMgr import WebsocketMgr, Subscription + + +LOG = logging.getLogger(__name__) + + +class CoinmateClient(CryptoXLibClient): + REST_API_URI = "https://coinmate.io/api/" + + def __init__(self, user_id: str = None, api_key: str = None, sec_key: str = None, api_trace_log: bool = False, + ssl_context: ssl.SSLContext = None) -> None: + super().__init__(api_trace_log, ssl_context) + + self.user_id = user_id + self.api_key = api_key + self.sec_key = sec_key + + def _get_rest_api_uri(self) -> str: + return self.REST_API_URI + + def _sign_payload(self, rest_call_type: RestCallType, resource: str, data: dict = None, params: dict = None, headers: dict = None) -> None: + nonce = self._get_current_timestamp_ms() + input_message = str(nonce) + str(self.user_id) + self.api_key + + m = hmac.new(self.sec_key.encode('utf-8'), input_message.encode('utf-8'), hashlib.sha256) + + params['signature'] = m.hexdigest().upper() + params['clientId'] = self.user_id + params['publicKey'] = self.api_key + params['nonce'] = nonce + + def _preprocess_rest_response(self, status_code: int, headers: 'CIMultiDictProxy[str]', body: Optional[dict]) -> None: + if str(status_code)[0] != '2': + raise CoinmateRestException(status_code, body) + else: + if "error" in body and body['error'] is True: + raise CoinmateRestException(status_code, body) + + def _get_websocket_mgr(self, subscriptions: List[Subscription], startup_delay_ms: int = 0, + ssl_context = None) -> WebsocketMgr: + return CoinmateWebsocket(subscriptions = subscriptions, + user_id = self.user_id, api_key = self.api_key, sec_key = self.sec_key, + ssl_context = ssl_context, + startup_delay_ms = startup_delay_ms) + + async def get_exchange_info(self) -> dict: + return await self._create_get("tradingPairs") + + async def get_currency_pairs(self) -> dict: + return await self._create_get("products") + + async def get_order_book(self, pair: Pair, group: bool = False) -> dict: + params = CoinmateClient._clean_request_params({ + "currencyPair": map_pair(pair), + "groupByPriceLimit": group + }) + + return await self._create_get("orderBook", params = params) + + async def get_ticker(self, pair: Pair) -> dict: + params = CoinmateClient._clean_request_params({ + "currencyPair": map_pair(pair) + }) + + return await self._create_get("ticker", params = params) + + async def get_transactions(self, minutes_history: int, currency_pair: Pair = None) -> dict: + params = CoinmateClient._clean_request_params({ + "minutesIntoHistory": minutes_history + }) + + if currency_pair is not None: + params['currencyPair'] = map_pair(currency_pair) + + return await self._create_get("transactions", params = params) + + async def get_balances(self) -> dict: + return await self._create_post("balances", signed = True) + + async def get_fees(self, pair: Pair = None) -> dict: + params = {} + if pair is not None: + params['currencyPair'] = map_pair(pair) + + return await self._create_post("traderFees", params = params, signed = True) + + async def get_transaction_history(self, offset: int = None, limit: int = None, ascending = False, order_id: str = None, + from_timestamp: datetime.datetime = None, to_timestamp: datetime.datetime = None) -> dict: + params = CoinmateClient._clean_request_params({ + "offset": offset, + "limit": limit, + "orderId": order_id + }) + + if ascending is True: + params['sort'] = 'ASC' + else: + params['sort'] = 'DESC' + + if from_timestamp is not None: + params["timestampFrom"] = from_timestamp.astimezone(pytz.utc).isoformat() + + if to_timestamp is not None: + params["timestampTo"] = to_timestamp.astimezone(pytz.utc).isoformat() + + return await self._create_post("transactionHistory", params = params, signed = True) + + async def get_trade_history(self, limit: int = None, ascending = False, order_id: str = None, last_id: str = None, + from_timestamp: datetime.datetime = None, to_timestamp: datetime.datetime = None, + pair: Pair = None) -> dict: + params = CoinmateClient._clean_request_params({ + "limit": limit, + "orderId": order_id, + "lastId": last_id + }) + + if pair is not None: + params['currencyPair'] = map_pair(pair) + + if ascending is True: + params['sort'] = 'ASC' + else: + params['sort'] = 'DESC' + + if from_timestamp is not None: + params["timestampFrom"] = from_timestamp.astimezone(pytz.utc).isoformat() + + if to_timestamp is not None: + params["timestampTo"] = to_timestamp.astimezone(pytz.utc).isoformat() + + return await self._create_post("tradeHistory", params = params, signed = True) + + async def get_transfer(self, transaction_id: str) -> dict: + params = CoinmateClient._clean_request_params({ + "transactionId": transaction_id + }) + + return await self._create_post("transfer", params = params, signed = True) + + async def get_transfer_history(self, limit: int = None, ascending = False, last_id: str = None, + from_timestamp: datetime.datetime = None, to_timestamp: datetime.datetime = None, + currency: str = None) -> dict: + params = CoinmateClient._clean_request_params({ + "limit": limit, + "lastId": last_id, + "currency": currency + }) + + if ascending is True: + params['sort'] = 'ASC' + else: + params['sort'] = 'DESC' + + if from_timestamp is not None: + params["timestampFrom"] = from_timestamp.astimezone(pytz.utc).isoformat() + + if to_timestamp is not None: + params["timestampTo"] = to_timestamp.astimezone(pytz.utc).isoformat() + + return await self._create_post("transferHistory", params = params, signed = True) + + async def get_order_history(self, pair: Pair, limit: int = None) -> dict: + params = CoinmateClient._clean_request_params({ + "currencyPair": map_pair(pair), + "limit": limit + }) + + return await self._create_post("orderHistory", params = params, signed = True) + + async def get_open_orders(self, pair: Pair = None) -> dict: + params = {} + + if pair is not None: + params["currencyPair"] = map_pair(pair) + + return await self._create_post("openOrders", params = params, signed = True) + + async def get_order(self, order_id: str = None, client_id: str = None) -> dict: + if not (bool(order_id) ^ bool(client_id)): + raise CoinmateException("One and only one of order_id and client_id can be provided.") + + params = {} + + if order_id is not None: + endpoint = "orderById" + params["orderId"] = order_id + else: + endpoint = "order" + params["clientOrderId"] = client_id + + return await self._create_post(endpoint, params = params, signed = True) + + async def cancel_order(self, order_id: str) -> dict: + params = { + "orderId": order_id + } + + return await self._create_post("cancelOrder", params = params, signed = True) + + async def cancel_all_orders(self, pair: Pair = None) -> dict: + params = {} + + if pair is not None: + params["currencyPair"] = map_pair(pair) + + return await self._create_post("cancelAllOpenOrders", params = params, signed = True) + + async def create_order(self, type: enums.OrderType, + pair: Pair, + side: enums.OrderSide, + amount: str, + price: str = None, + stop_price: str = None, + trailing: bool = None, + hidden: bool = None, + time_in_force: enums.TimeInForce = None, + post_only: bool = None, + client_id: str = None) -> dict: + params = CoinmateClient._clean_request_params({ + "currencyPair": map_pair(pair), + "price": price, + "stopPrice": stop_price, + "clientOrderId": client_id + }) + + if type == enums.OrderType.MARKET: + if side == enums.OrderSide.BUY: + endpoint = "buyInstant" + params["total"] = amount + else: + endpoint = "sellInstant" + params["amount"] = amount + else: + if side == enums.OrderSide.BUY: + endpoint = "buyLimit" + else: + endpoint = "sellLimit" + + params["amount"] = amount + + if trailing is True: + params["trailing"] = "1" + + if hidden is True: + params["hidden"] = "1" + + if post_only is True: + params["postOnly"] = "1" + + if time_in_force == enums.TimeInForce.IMMEDIATE_OR_CANCELLED: + params["immediateOrCancel"] = "1" + + return await self._create_post(endpoint, params = params, signed = True) \ No newline at end of file diff --git a/cryptoxlib/clients/coinmate/CoinmateWebsocket.py b/cryptoxlib/clients/coinmate/CoinmateWebsocket.py new file mode 100644 index 0000000..f34ce1e --- /dev/null +++ b/cryptoxlib/clients/coinmate/CoinmateWebsocket.py @@ -0,0 +1,193 @@ +import json +import logging +import datetime +import hmac +import hashlib +from typing import List, Any + +from cryptoxlib.WebsocketMgr import Subscription, WebsocketMgr, WebsocketMessage, Websocket, CallbacksType +from cryptoxlib.Pair import Pair +from cryptoxlib.clients.coinmate.functions import map_pair +from cryptoxlib.clients.coinmate.exceptions import CoinmateException + +LOG = logging.getLogger(__name__) + + +class CoinmateWebsocket(WebsocketMgr): + WEBSOCKET_URI = "wss://coinmate.io/api/websocket" + MAX_MESSAGE_SIZE = 3 * 1024 * 1024 # 3MB + + def __init__(self, subscriptions: List[Subscription], + user_id: str = None, api_key: str = None, sec_key: str = None, + ssl_context = None, + startup_delay_ms: int = 0) -> None: + super().__init__(websocket_uri = self.WEBSOCKET_URI, subscriptions = subscriptions, + max_message_size = CoinmateWebsocket.MAX_MESSAGE_SIZE, + ssl_context = ssl_context, + auto_reconnect = True, + builtin_ping_interval = None, + startup_delay_ms = startup_delay_ms) + + self.user_id = user_id + self.api_key = api_key + self.sec_key = sec_key + + def get_websocket(self) -> Websocket: + return self.get_aiohttp_websocket() + + async def initialize_subscriptions(self, subscriptions: List[Subscription]) -> None: + for subscription in subscriptions: + await subscription.initialize(user_id = self.user_id) + + async def send_subscription_message(self, subscriptions: List[Subscription]): + for subscription in subscriptions: + subscription_message = { + "event": "subscribe", + "data": { + "channel": subscription.get_subscription_message() + } + } + + if subscription.requires_authentication(): + nonce = int(datetime.datetime.now(tz = datetime.timezone.utc).timestamp() * 1000) # timestamp ms + input_message = str(nonce) + str(self.user_id) + self.api_key + + m = hmac.new(self.sec_key.encode('utf-8'), input_message.encode('utf-8'), hashlib.sha256) + + subscription_message['data']['signature'] = m.hexdigest().upper() + subscription_message['data']['clientId'] = self.user_id + subscription_message['data']['publicKey'] = self.api_key + subscription_message['data']['nonce'] = nonce + + LOG.debug(f"> {subscription_message}") + await self.websocket.send(json.dumps(subscription_message)) + + message = await self.websocket.receive() + LOG.debug(f"< {message}") + + message = json.loads(message) + if message['event'] == 'subscribe_success': + LOG.info(f"Channel {subscription.get_subscription_message()} subscribed successfully.") + else: + raise CoinmateException(f"Subscription failed for channel {subscription.get_subscription_message()}. Response [{message}]") + + + async def send_unsubscription_message(self, subscriptions: List[Subscription]): + unsubscription_message = self._get_unsubscription_message(subscriptions) + + LOG.debug(f"> {unsubscription_message}") + await self.websocket.send(json.dumps(unsubscription_message)) + + async def _process_message(self, websocket: Websocket, message: str) -> None: + message = json.loads(message) + + # data message + if "event" in message and message['event'] == "data": + await self.publish_message(WebsocketMessage( + subscription_id = message['channel'], + message = message + )) + + +class CoinmateSubscription(Subscription): + def __init__(self, callbacks: CallbacksType = None): + super().__init__(callbacks) + + self.user_id = None + + def construct_subscription_id(self) -> Any: + return self.get_subscription_message() + + def requires_authentication(self) -> bool: + return False + + async def initialize(self, **kwargs): + self.user_id = kwargs['user_id'] + + +class UserOrdersSubscription(CoinmateSubscription): + def __init__(self, pair: Pair = None, callbacks: CallbacksType = None): + super().__init__(callbacks) + + self.pair = pair + + def get_subscription_message(self, **kwargs) -> dict: + msg = f"private-open_orders-{self.user_id}" + + if self.pair is not None: + msg += f"-{map_pair(self.pair)}" + + return msg + + def requires_authentication(self) -> bool: + return True + + +class UserTradesSubscription(CoinmateSubscription): + def __init__(self, pair: Pair = None, callbacks: CallbacksType = None): + super().__init__(callbacks) + + self.pair = pair + + def get_subscription_message(self, **kwargs) -> dict: + msg = f"private-user-trades-{self.user_id}" + + if self.pair is not None: + msg += f"-{map_pair(self.pair)}" + + return msg + + def requires_authentication(self) -> bool: + return True + + +class UserTransfersSubscription(CoinmateSubscription): + def __init__(self, callbacks: CallbacksType = None): + super().__init__(callbacks) + + def get_subscription_message(self, **kwargs) -> dict: + return f"private-user-transfers-{self.user_id}" + + def requires_authentication(self) -> bool: + return True + + +class BalancesSubscription(CoinmateSubscription): + def __init__(self, callbacks: CallbacksType = None): + super().__init__(callbacks) + + def get_subscription_message(self, **kwargs) -> dict: + return f"private-user_balances-{self.user_id}" + + def requires_authentication(self) -> bool: + return True + + +class OrderbookSubscription(CoinmateSubscription): + def __init__(self, pair: Pair, callbacks: CallbacksType = None): + super().__init__(callbacks) + + self.pair = pair + + def get_subscription_message(self, **kwargs) -> dict: + return f"order_book-{map_pair(self.pair)}" + + +class TradesSubscription(CoinmateSubscription): + def __init__(self, pair: Pair, callbacks: CallbacksType = None): + super().__init__(callbacks) + + self.pair = pair + + def get_subscription_message(self, **kwargs) -> dict: + return f"trades-{map_pair(self.pair)}" + + +class TradeStatsSubscription(CoinmateSubscription): + def __init__(self, pair: Pair, callbacks: CallbacksType = None): + super().__init__(callbacks) + + self.pair = pair + + def get_subscription_message(self, **kwargs) -> dict: + return f"statistics-{map_pair(self.pair)}" \ No newline at end of file diff --git a/cryptoxlib/clients/coinmate/__init__.py b/cryptoxlib/clients/coinmate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoxlib/clients/coinmate/enums.py b/cryptoxlib/clients/coinmate/enums.py new file mode 100644 index 0000000..291672b --- /dev/null +++ b/cryptoxlib/clients/coinmate/enums.py @@ -0,0 +1,16 @@ +import enum + + +class OrderType(enum.Enum): + MARKET = enum.auto() + LIMIT = enum.auto() + + +class OrderSide(enum.Enum): + BUY = enum.auto() + SELL = enum.auto() + + +class TimeInForce(enum.Enum): + GOOD_TILL_CANCELLED = enum.auto() + IMMEDIATE_OR_CANCELLED = enum.auto() \ No newline at end of file diff --git a/cryptoxlib/clients/coinmate/exceptions.py b/cryptoxlib/clients/coinmate/exceptions.py new file mode 100644 index 0000000..cffe469 --- /dev/null +++ b/cryptoxlib/clients/coinmate/exceptions.py @@ -0,0 +1,15 @@ +from typing import Optional + +from cryptoxlib.exceptions import CryptoXLibException + + +class CoinmateException(CryptoXLibException): + pass + + +class CoinmateRestException(CoinmateException): + def __init__(self, status_code: int, body: Optional[dict]): + super().__init__(f"Rest API exception: status [{status_code}], response [{body}]") + + self.status_code = status_code + self.body = body \ No newline at end of file diff --git a/cryptoxlib/clients/coinmate/functions.py b/cryptoxlib/clients/coinmate/functions.py new file mode 100644 index 0000000..ddd03ec --- /dev/null +++ b/cryptoxlib/clients/coinmate/functions.py @@ -0,0 +1,5 @@ +from cryptoxlib.Pair import Pair + + +def map_pair(pair: Pair) -> str: + return f"{pair.base}_{pair.quote}" diff --git a/examples/coinmate_rest_api.py b/examples/coinmate_rest_api.py new file mode 100644 index 0000000..b7d9834 --- /dev/null +++ b/examples/coinmate_rest_api.py @@ -0,0 +1,39 @@ +import logging +import datetime +import os + +from cryptoxlib.CryptoXLib import CryptoXLib +from cryptoxlib.Pair import Pair +from cryptoxlib.clients.bitpanda.exceptions import BitpandaException +from cryptoxlib.clients.bitpanda.enums import OrderSide, TimeUnit +from cryptoxlib.version_conversions import async_run + +LOG = logging.getLogger("cryptoxlib") +LOG.setLevel(logging.DEBUG) +LOG.addHandler(logging.StreamHandler()) + +print(f"Available loggers: {[name for name in logging.root.manager.loggerDict]}") + + +async def order_book_update(response: dict) -> None: + print(f"Callback order_book_update: [{response}]") + +async def run(): + api_key = os.environ['BITPANDAAPIKEY'] + + client = CryptoXLib.create_coinmate_client(api_key, "", "") + + print("Trading pairs:") + await client.get_exchange_info() + + print("Currency pairs:") + await client.get_currency_pairs() + + print("Ticker:") + await client.get_ticker(pair = Pair('BTC', 'EUR')) + + await client.close() + + +if __name__ == "__main__": + async_run(run()) diff --git a/examples/coinmate_ws.py b/examples/coinmate_ws.py new file mode 100644 index 0000000..73a50bb --- /dev/null +++ b/examples/coinmate_ws.py @@ -0,0 +1,54 @@ +import logging +import os + +from cryptoxlib.CryptoXLib import CryptoXLib +from cryptoxlib.Pair import Pair +from cryptoxlib.clients.coinmate.CoinmateWebsocket import OrderbookSubscription, TradesSubscription, \ + UserOrdersSubscription, BalancesSubscription, UserTradesSubscription, UserTransfersSubscription +from cryptoxlib.version_conversions import async_run + +LOG = logging.getLogger("cryptoxlib") +LOG.setLevel(logging.DEBUG) +LOG.addHandler(logging.StreamHandler()) + +print(f"Available loggers: {[name for name in logging.root.manager.loggerDict]}\n") + + +async def order_book_update(response: dict) -> None: + print(f"Callback order_book_update: [{response}]") + + +async def trades_update(response: dict) -> None: + print(f"Callback trades_update: [{response}]") + + +async def account_update(response: dict) -> None: + print(f"Callback account_update: [{response}]") + + +async def run(): + api_key = os.environ['COINMATEAPIKEY'] + sec_key = os.environ['COINMATESECKEY'] + user_id = os.environ['COINMATEUSERID'] + + client = CryptoXLib.create_coinmate_client(user_id, api_key, sec_key) + + # Bundle several subscriptions into a single websocket + client.compose_subscriptions([ + OrderbookSubscription(Pair("BTC", "EUR"), callbacks = [order_book_update]), + TradesSubscription(Pair("BTC", "EUR"), callbacks = [trades_update]) + ]) + + # Bundle private subscriptions into a separate websocket + client.compose_subscriptions([ + UserOrdersSubscription(callbacks = [account_update]), + UserTradesSubscription(callbacks = [account_update]), + UserTransfersSubscription(callbacks = [account_update]), + BalancesSubscription(callbacks = [account_update]) + ]) + + # Execute all websockets asynchronously + await client.start_websockets() + +if __name__ == "__main__": + async_run(run()) diff --git a/setup.py b/setup.py index b8ca756..0def970 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setuptools.setup( name="cryptoxlib-aio", - version="5.1.6", + version="5.2.0", author="nardew", author_email="cryptoxlib.aio@gmail.com", description="Cryptoexchange asynchronous python client", @@ -25,6 +25,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", diff --git a/tests/e2e/coinmate.py b/tests/e2e/coinmate.py new file mode 100644 index 0000000..6fd4a5c --- /dev/null +++ b/tests/e2e/coinmate.py @@ -0,0 +1,192 @@ +import unittest +import os +import logging +import datetime + +from cryptoxlib.CryptoXLib import CryptoXLib +from cryptoxlib.Pair import Pair +from cryptoxlib.clients.coinmate.exceptions import CoinmateRestException +from cryptoxlib.clients.coinmate.CoinmateWebsocket import OrderbookSubscription +from cryptoxlib.clients.coinmate import enums + +from CryptoXLibTest import CryptoXLibTest, WsMessageCounter + +api_key = os.environ['COINMATEAPIKEY'] +sec_key = os.environ['COINMATESECKEY'] +user_id = os.environ['COINMATEUSERID'] + + +class CoinmateRestApi(CryptoXLibTest): + @classmethod + def initialize(cls) -> None: + cls.print_logs = True + cls.log_level = logging.DEBUG + + def check_positive_response(self, response): + return str(response['status_code'])[0] == '2' + + async def init_test(self): + self.client = CryptoXLib.create_coinmate_client(user_id, api_key, sec_key) + + async def clean_test(self): + await self.client.close() + + async def test_get_pairs(self): + response = await self.client.get_exchange_info() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_order_book(self): + response = await self.client.get_order_book(pair = Pair("BTC", "EUR")) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_order_book2(self): + response = await self.client.get_order_book(pair = Pair("BTC", "EUR"), group = True) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_ticker(self): + response = await self.client.get_ticker(pair = Pair("BTC", "EUR")) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_currency_pairs(self): + response = await self.client.get_currency_pairs() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_transactions(self): + response = await self.client.get_transactions(minutes_history = 10) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_transactions2(self): + response = await self.client.get_transactions(minutes_history = 10, currency_pair = Pair("BTC", "EUR")) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_balances(self): + response = await self.client.get_balances() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_fees(self): + response = await self.client.get_fees() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_fees2(self): + response = await self.client.get_fees(pair = Pair('BTC', 'EUR')) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_transaction_history(self): + response = await self.client.get_transaction_history() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_trade_history(self): + response = await self.client.get_trade_history() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_trade_history2(self): + response = await self.client.get_trade_history(pair = Pair('BTC', 'EUR')) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_transfer(self): + with self.assertRaises(CoinmateRestException) as cm: + await self.client.get_transfer(transaction_id = "0") + e = cm.exception + + self.assertEqual(e.status_code, 200) + self.assertTrue(e.body['errorMessage'] == 'No transfer with given ID') + + async def test_get_transfer_history(self): + response = await self.client.get_transfer_history() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_transfer_history2(self): + response = await self.client.get_transfer_history(currency = 'EUR') + self.assertTrue(self.check_positive_response(response)) + + async def test_get_order_history(self): + response = await self.client.get_order_history(pair = Pair('BTC', 'EUR')) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_open_orders(self): + response = await self.client.get_open_orders() + self.assertTrue(self.check_positive_response(response)) + + async def test_get_open_orders2(self): + response = await self.client.get_open_orders(pair = Pair('BTC', 'EUR')) + self.assertTrue(self.check_positive_response(response)) + + async def test_get_order(self): + with self.assertRaises(CoinmateRestException) as cm: + await self.client.get_order(order_id = "0") + e = cm.exception + + self.assertEqual(e.status_code, 200) + self.assertTrue(e.body['errorMessage'] == 'No order with given ID') + + async def test_get_order2(self): + response = await self.client.get_order(client_id = "0") + self.assertTrue(self.check_positive_response(response)) + + async def test_cancel_order(self): + response = await self.client.cancel_order(order_id = "0") + self.assertTrue(self.check_positive_response(response)) + + async def test_cancel_all_orders(self): + response = await self.client.cancel_all_orders() + self.assertTrue(self.check_positive_response(response)) + + async def test_cancel_all_orders2(self): + response = await self.client.cancel_all_orders(pair = Pair('BTC', 'EUR')) + self.assertTrue(self.check_positive_response(response)) + + async def test_create_order(self): + with self.assertRaises(CoinmateRestException) as cm: + await self.client.create_order(type = enums.OrderType.MARKET, side = enums.OrderSide.BUY, amount = "100", + pair = Pair('BTC', 'EUR')) + e = cm.exception + + self.assertEqual(e.status_code, 200) + + async def test_create_order2(self): + with self.assertRaises(CoinmateRestException) as cm: + await self.client.create_order(type = enums.OrderType.MARKET, side = enums.OrderSide.SELL, amount = "100", + pair = Pair('BTC', 'EUR')) + e = cm.exception + + self.assertEqual(e.status_code, 200) + + async def test_create_order3(self): + with self.assertRaises(CoinmateRestException) as cm: + await self.client.create_order(type = enums.OrderType.LIMIT, side = enums.OrderSide.BUY, price = "1", + amount = "100", + pair = Pair('BTC', 'EUR')) + e = cm.exception + + self.assertEqual(e.status_code, 200) + + async def test_create_order4(self): + with self.assertRaises(CoinmateRestException) as cm: + await self.client.create_order(type = enums.OrderType.LIMIT, side = enums.OrderSide.SELL, price = "1000000", + amount = "0.001", + pair = Pair('BTC', 'EUR')) + e = cm.exception + + self.assertEqual(e.status_code, 200) + + +class CoinmateWs(CryptoXLibTest): + @classmethod + def initialize(cls) -> None: + cls.print_logs = True + cls.log_level = logging.DEBUG + + async def init_test(self): + self.client = CryptoXLib.create_coinmate_client(user_id, api_key, sec_key) + + async def test_order_book_subscription(self): + message_counter = WsMessageCounter() + self.client.compose_subscriptions([ + OrderbookSubscription(Pair("BTC", "EUR"), callbacks = [message_counter.generate_callback(1)]) + ]) + + await self.assertWsMessageCount(message_counter) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file