Skip to content

Commit

Permalink
Coinmate (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
nardew committed Sep 19, 2021
1 parent de411a4 commit db78dbf
Show file tree
Hide file tree
Showing 14 changed files with 804 additions and 5 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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) |
Expand Down
7 changes: 6 additions & 1 deletion cryptoxlib/CryptoXLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
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)
4 changes: 3 additions & 1 deletion cryptoxlib/CryptoXLibClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
268 changes: 268 additions & 0 deletions cryptoxlib/clients/coinmate/CoinmateClient.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit db78dbf

Please sign in to comment.