From 47a5efb47279cf0fd85119ea066a1c34fd819c6f Mon Sep 17 00:00:00 2001 From: Felipe Aranguiz Date: Fri, 28 Apr 2017 16:18:28 -0300 Subject: [PATCH] api v2 changes --- README.md | 11 +- requierements.txt | 4 +- setup.py | 2 +- surbtc/__init__.py | 28 +++- surbtc/base.py | 26 ++- surbtc/client.py | 207 ------------------------ surbtc/client_auth.py | 241 ++++++++++++++++++++++++++++ surbtc/client_public.py | 38 +++++ surbtc/common.py | 5 +- surbtc/constants.py | 93 +++++++++++ surbtc/models.py | 341 ++++++++++++++++++++++++++++++++++++++++ surbtc/server.py | 15 ++ test/surbtc_test.py | 144 ++++++++++------- 13 files changed, 874 insertions(+), 281 deletions(-) delete mode 100644 surbtc/client.py create mode 100644 surbtc/client_auth.py create mode 100644 surbtc/client_public.py create mode 100644 surbtc/constants.py create mode 100644 surbtc/models.py create mode 100644 surbtc/server.py diff --git a/README.md b/README.md index 285da88..856f675 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,17 @@ Rename .env.example > .env ## Usage -Setup (ApiKey/Secret requiered): +###Setup Public: from surbtc import SURBTC - client = SURBTC(API_KEY, API_SECRET) + client = SURBTC.Public() -Market Pairs: +###Setup Auth (ApiKey/Secret requiered, Test is optional (default: False)): + + from surbtc import SURBTC + client = SURBTC.Auth(API_KEY, API_SECRET, TEST) + +## Market Pairs: btc-clp btc-cop diff --git a/requierements.txt b/requierements.txt index 59545e2..43c0f1f 100644 --- a/requierements.txt +++ b/requierements.txt @@ -1,3 +1,3 @@ -coverage==4.3 +coverage==4.3.4 python-decouple==3.0 -requests==2.12.4 +requests==2.13.0 diff --git a/setup.py b/setup.py index 50213a4..2685790 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='surbtc', - version='0.1.0', + version='0.2.0', description='SURBTC API Wrapper for Python 3', url='https://github.com/delta575/python-surbtc-api', author='Felipe Aránguiz, Sebastian Aránguiz', diff --git a/surbtc/__init__.py b/surbtc/__init__.py index 6283aa7..a228750 100644 --- a/surbtc/__init__.py +++ b/surbtc/__init__.py @@ -1,3 +1,27 @@ -from .client import SURBTC +from . import constants as _c +from . import models as _m +from .client_auth import SURBTCAuth +from .client_public import SURBTCPublic -__all__ = SURBTC + +class SURBTC(object): + # Models + models = _m + # Enum Types + BalanceEvent = _c.BalanceEvent + Currency = _c.Currency + Market = _c.Market + OrderState = _c.OrderState + OrderType = _c.OrderType + OrderPriceType = _c.OrderPriceType + QuotationType = _c.QuotationType + ReportType = _c.ReportType + # Clients + Auth = SURBTCAuth + Public = SURBTCPublic + + +__all__ = [ + SURBTCAuth, + SURBTCPublic, +] \ No newline at end of file diff --git a/surbtc/base.py b/surbtc/base.py index a20ea6f..2ee77ad 100644 --- a/surbtc/base.py +++ b/surbtc/base.py @@ -1,14 +1,15 @@ import json from urllib.parse import urlparse + # pip import requests # local -from .common import (check_response, - log_request_exception, - log_json_decode) +from .common import (check_response, log_json_decode, + log_request_exception) class Server(object): + def __init__(self, protocol, host, version=None): url = '{0:s}://{1:s}'.format(protocol, host) if version: @@ -21,6 +22,7 @@ def __init__(self, protocol, host, version=None): class Client(object): + def __init__(self, server: Server, timeout=30): self.SERVER = server self.TIMEOUT = timeout @@ -42,17 +44,25 @@ def post(self, url, headers, data): def _request(self, method, url, headers, params=None, data=None): try: - data = json.dumps(data) if data else data + data = self._encode_data(data) response = requests.request( - method, url, headers=headers, params=params, data=data, - verify=True, timeout=self.TIMEOUT - ) + method, + url, + headers=headers, + params=params, + data=data, + verify=True, + timeout=self.TIMEOUT) response.raise_for_status() return response except requests.exceptions.RequestException as err: log_request_exception(err) raise + def _encode_data(self, data): + data = json.dumps(data) if data else data + return data + def _resp_to_json(self, response): try: json_resp = response.json() @@ -71,4 +81,4 @@ def url_for(self, path, path_arg=None): def url_path_for(self, path, path_arg=None): url = self.url_for(path, path_arg) path = urlparse(url).path - return url, path + return url, path \ No newline at end of file diff --git a/surbtc/client.py b/surbtc/client.py deleted file mode 100644 index 2c6d6d8..0000000 --- a/surbtc/client.py +++ /dev/null @@ -1,207 +0,0 @@ -import base64 -import hashlib -import hmac -import json -# pip -from requests import RequestException -# local -from .common import check_keys, build_route, gen_nonce -from .base import Client, Server - -# API server -PROTOCOL = 'https' -HOST = 'www.surbtc.com/api' -TEST_HOST = 'stg.surbtc.com/api' -VERSION = 'v1' - -# API paths -PATH_MARKETS = 'markets' -PATH_MARKET_DETAILS = 'markets/%s' -PATH_INDICATORS = "markets/%s/indicators" -PATH_ORDER_BOOK = 'markets/%s/order_book' -PATH_QUOTATION = 'markets/%s/quotations' -PATH_FEE_PERCENTAGE = 'markets/%s/fee_percentage' -PATH_TRADE_TRANSACTIONS = 'markets/%s/trade_transactions' -PATH_REPORTS = 'markets/%s/reports' -PATH_BALANCES = 'balances/%s' -PATH_BALANCES_EVENTS = 'balance_events' -PATH_ORDERS = 'markets/%s/orders' -PATH_SINGLE_ORDER = 'orders/%s' -PATH_WITHDRAWAL = 'withdrawals' - - -class SURBTC(Client): - - def __init__(self, key=False, secret=False, test=False, timeout=30): - server = Server(PROTOCOL, HOST if not test else TEST_HOST, VERSION) - Client.__init__(self, server, timeout) - check_keys(key, secret) - self.KEY = str(key) - self.SECRET = str(secret) - - def live(self): - try: - self.markets() - return True - except RequestException: - return False - - # MARKETS------------------------------------------------------------------ - def markets(self): - url, path = self.url_path_for(PATH_MARKETS) - headers = self._sign_payload(method='GET', path=path) - return self.get(url, headers=headers) - - def market_details(self, market_id): - url, path = self.url_path_for(PATH_MARKET_DETAILS, path_arg=market_id) - headers = self._sign_payload(method='GET', path=path) - return self.get(url, headers=headers) - - def indicators(self, market_id): - url, path = self.url_path_for(PATH_INDICATORS, path_arg=market_id) - headers = self._sign_payload(method='GET', path=path) - return self.get(url, headers=headers) - - def order_book(self, market_id): - url, path = self.url_path_for(PATH_ORDER_BOOK, path_arg=market_id) - headers = self._sign_payload(method='GET', path=path) - return self.get(url, headers=headers) - - def quotation(self, market_id, quotation_type, reverse, amount): - payload = { - 'quotation': { - 'type': quotation_type, - 'reverse': reverse, - 'amount': str(amount), - }, - } - url, path = self.url_path_for(PATH_QUOTATION, path_arg=market_id) - headers = self._sign_payload(method='POST', path=path, payload=payload) - return self.post(url, headers=headers, data=payload) - - def fee_percentage(self, market_id, order_type, market_order=False): - params = { - 'type': order_type, - 'market_order': market_order, - } - url, path = self.url_path_for(PATH_FEE_PERCENTAGE, path_arg=market_id) - headers = self._sign_payload(method='GET', path=path, params=params) - return self.get(url, headers=headers, params=params) - - def trade_transactions(self, market_id, page=None, per_page=None): - params = { - 'page': page, - 'per_page': per_page, - } - url, path = self.url_path_for(PATH_TRADE_TRANSACTIONS, - path_arg=market_id) - headers = self._sign_payload(method='GET', path=path, params=params) - return self.get(url, headers=headers, params=params) - - def reports(self, market_id, report_type, from_timestamp=None, - to_timestamp=None): - params = { - 'report_type': report_type, - 'from': from_timestamp, - 'to': to_timestamp, - } - url, path = self.url_path_for(PATH_REPORTS, path_arg=market_id) - headers = self._sign_payload(method='GET', path=path, params=params) - return self.get(url, headers=headers, params=params) - - # BALANCES----------------------------------------------------------------- - def balance(self, currency): - url, path = self.url_path_for(PATH_BALANCES, path_arg=currency) - headers = self._sign_payload(method='GET', path=path) - return self.get(url, headers=headers) - - # Call with 'page' param return authentication error - def balance_events(self, currencies, event_names, page=None, per_page=None, - relevant=None): - params = { - 'currencies[]': currencies, - 'event_names[]': event_names, - 'page': page, - 'per': per_page, - 'relevant': relevant, - } - url, path = self.url_path_for(PATH_BALANCES_EVENTS) - headers = self._sign_payload(method='GET', path=path, params=params) - return self.get(url, headers=headers, params=params) - - # ORDERS------------------------------------------------------------------- - def new_order(self, market_id, order_type, limit, amount, price_type): - payload = { - 'type': order_type, - 'limit': limit, - 'amount': amount, - 'price_type': price_type, - } - return self.new_order_payload(market_id, payload) - - def new_order_payload(self, market, payload): - url, path = self.url_path_for(PATH_ORDERS, path_arg=market) - headers = self._sign_payload(method='POST', path=path, payload=payload) - return self.post(url, headers=headers, data=payload) - - def orders(self, market_id, page=None, per_page=None, state=None): - params = { - 'per': per_page, - 'page': page, - 'state': state, - } - url, path = self.url_path_for(PATH_ORDERS, path_arg=market_id) - headers = self._sign_payload(method='GET', path=path, params=params) - return self.get(url, headers=headers, params=params) - - def single_order(self, order_id): - url, path = self.url_path_for(PATH_SINGLE_ORDER, path_arg=order_id) - headers = self._sign_payload(method='GET', path=path) - return self.get(url, headers=headers) - - def cancel_order(self, order_id): - payload = { - 'state': 'canceling', - } - url, path = self.url_path_for(PATH_SINGLE_ORDER, path_arg=order_id) - headers = self._sign_payload(method='PUT', path=path, payload=payload) - return self.put(url, headers=headers, data=payload) - - # PAYMENTS----------------------------------------------------------------- - def withdraw(self, target_address, amount, currency='BTC'): - payload = { - 'withdrawal_data': { - 'target_address': target_address, - }, - 'amount': str(amount), - 'currency': currency, - } - url, path = self.url_path_for(PATH_WITHDRAWAL) - headers = self._sign_payload(method='POST', path=path, payload=payload) - return self.post(url, headers=headers, data=payload) - - # PRIVATE METHODS---------------------------------------------------------- - def _sign_payload(self, method, path, params=None, payload=None): - - route = build_route(path, params) - nonce = gen_nonce() - - if payload: - j = json.dumps(payload).encode('utf-8') - encoded_body = base64.standard_b64encode(j).decode('utf-8') - string = method + ' ' + route + ' ' + encoded_body + ' ' + nonce - else: - string = method + ' ' + route + ' ' + nonce - - h = hmac.new(key=self.SECRET.encode('utf-8'), - msg=string.encode('utf-8'), - digestmod=hashlib.sha384) - - signature = h.hexdigest() - - return { - 'X-SBTC-APIKEY': self.KEY, - 'X-SBTC-NONCE': nonce, - 'X-SBTC-SIGNATURE': signature, - 'Content-Type': 'application/json', - } diff --git a/surbtc/client_auth.py b/surbtc/client_auth.py new file mode 100644 index 0000000..87954ef --- /dev/null +++ b/surbtc/client_auth.py @@ -0,0 +1,241 @@ +import base64 +from datetime import datetime +import hashlib +import hmac +import json + +# local +from .common import build_route, check_keys, gen_nonce + +from . import constants as _c +from . import models as _m +from .client_public import SURBTCPublic + +_p = _c.Path + + +class SURBTCAuth(SURBTCPublic): + + def __init__(self, key=False, secret=False, test=False, timeout=30): + SURBTCPublic.__init__(self, test, timeout) + check_keys(key, secret) + self.KEY = str(key) + self.SECRET = str(secret) + + def quotation(self, + market_id: _c.Market, + currency: _c.Currency, + quotation_type: _c.QuotationType, + price_limit: float, + amount: float): + market_id = _c.Market.check(market_id) + currency = _c.Currency.check(currency) + quotation_type = _c.QuotationType.check(quotation_type) + payload = { + 'quotation': { + 'type': quotation_type.value, + 'limit': str([str(price_limit), currency.value]), + 'amount': str([str(amount), currency.value]) + }, + } + url, path = self.url_path_for(_p.QUOTATION, + path_arg=market_id.value) + headers = self._sign_payload(method='POST', path=path, payload=payload) + data = self.post(url, headers=headers, data=payload) + return _m.Quotation.create_from_json(data['quotation']) + + def fee_percentage(self, + market_id: _c.Market, + order_type: _c.OrderType, + market_order: bool = False): + market_id = _c.Market.check(market_id) + order_type = _c.OrderType.check(order_type) + params = { + 'type': order_type.value, + 'market_order': market_order, + } + url, path = self.url_path_for(_p.FEE_PERCENTAGE, + path_arg=market_id.value) + headers = self._sign_payload(method='GET', path=path, params=params) + data = self.get(url, headers=headers, params=params) + return _m.FeePercentage.create_from_json(data['fee_percentage']) + + def trade_transaction_pages(self, + market_id: _c.Market, + page: int = None, + per_page: int = None): + market_id = _c.Market.check(market_id) + # TODO: Pagination isn't working, it always returns 25 items + params = { + 'page': page, + 'per': per_page, + } + url, path = self.url_path_for(_p.TRADE_TRANSACTIONS, + path_arg=market_id.value) + headers = self._sign_payload(method='GET', path=path, params=params) + data = self.get(url, headers=headers, params=params) + # TODO: Response doesn't contain a meta field + return [_m.TradeTransaction.create_from_json(transaction) + for transaction in data['trade_transactions']] + + # REPORTS ----------------------------------------------------------------- + def report(self, + market_id: _c.Market, + report_type: _c.ReportType, + start_at: datetime=None, + end_at: datetime=None): + market_id = _c.Market.check(market_id) + report_type = _c.ReportType.check(report_type) + if isinstance(start_at, datetime): + start_at = int(start_at.timestamp()) + if isinstance(end_at, datetime): + end_at = int(end_at.timestamp()) + params = { + 'report_type': report_type.value, + 'from': start_at, + 'to': end_at, + } + url, path = self.url_path_for(_p.REPORTS, + path_arg=market_id.value) + headers = self._sign_payload(method='GET', path=path, params=params) + data = self.get(url, headers=headers, params=params) + # TODO: Report doesn't have a model + return data + + # BALANCES----------------------------------------------------------------- + def balance(self, currency: _c.Currency): + currency = _c.Currency.check(currency) + url, path = self.url_path_for(_p.BALANCES, path_arg=currency.value) + headers = self._sign_payload(method='GET', path=path) + data = self.get(url, headers=headers) + return _m.Balance.create_from_json(data['balance']) + + def balance_event_pages(self, + currencies, + event_names, + page: int = None, + per_page: int = None, + relevant: bool = None): + currencies = [_c.Currency.check(c).value + for c in currencies] + event_names = [_c.BalanceEvent.check(e).value + for e in event_names] + params = { + 'currencies[]': currencies, + 'event_names[]': event_names, + 'page': page, + 'per': per_page, + 'relevant': relevant, + } + url, path = self.url_path_for(_p.BALANCES_EVENTS) + headers = self._sign_payload(method='GET', path=path, params=params) + data = self.get(url, headers=headers, params=params) + # TODO: Response only contains a 'total_count' field instead of meta + return _m.BalanceEventPages.create_from_json( + data['balance_events'], data['total_count'], page) + + # ORDERS ------------------------------------------------------------------ + def new_order(self, + market_id: _c.Market, + order_type: _c.OrderType, + price_type: _c.OrderPriceType, + amount: float, + limit: float = None): + market_id = _c.Market.check(market_id) + order_type = _c.OrderType.check(order_type) + price_type = _c.OrderPriceType.check(price_type) + payload = { + 'type': order_type.value, + 'price_type': price_type.value, + 'amount': str(amount), + 'limit': str(limit), + } + return self.new_order_payload(market_id, payload) + + def new_order_payload(self, market_id: _c.Market, payload): + market_id = _c.Market.check(market_id) + url, path = self.url_path_for(_p.ORDERS, path_arg=market_id.value) + headers = self._sign_payload(method='POST', path=path, payload=payload) + data = self.post(url, headers=headers, data=payload) + return _m.Order.create_from_json(data['order']) + + def order_pages(self, + market_id: _c.Market, + page: int = None, + per_page: int = None, + state: _c.OrderState = None, + minimum_exchanged: float = None): + market_id = _c.Market.check(market_id) + state = _c.OrderState.check(state) + if per_page and per_page > _c.ORDERS_LIMIT: + msg = "Param 'per_page' must be <= {0}".format(_c.ORDERS_LIMIT) + raise ValueError(msg) + params = { + 'per': per_page, + 'page': page, + 'state': state.value if state else state, + # TODO: API has a typo, 'minimun' must be 'minimum' + 'minimun_exchanged': minimum_exchanged, + } + url, path = self.url_path_for(_p.ORDERS, + path_arg=market_id.value) + headers = self._sign_payload(method='GET', path=path, params=params) + data = self.get(url, headers=headers, params=params) + return _m.OrderPages.create_from_json(data['orders'], data['meta']) + + def order_details(self, order_id: int): + url, path = self.url_path_for(_p.SINGLE_ORDER, + path_arg=order_id) + headers = self._sign_payload(method='GET', path=path) + data = self.get(url, headers=headers) + return _m.Order.create_from_json(data['order']) + + def cancel_order(self, order_id: int): + payload = { + 'state': _c.OrderState.CANCELING.value, + } + url, path = self.url_path_for(_p.SINGLE_ORDER, + path_arg=order_id) + headers = self._sign_payload(method='PUT', path=path, payload=payload) + data = self.put(url, headers=headers, data=payload) + return _m.Order.create_from_json(data['order']) + + # PAYMENTS ---------------------------------------------------------------- + # TODO: UNTESTED + def withdraw(self, amount, currency, target_address=None): + payload = { + 'withdrawal_data': { + 'target_address': target_address, + }, + 'amount': amount, + 'currency': currency, + } + url, path = self.url_path_for(_p.WITHDRAWAL, path_arg=currency) + headers = self._sign_payload(method='POST', path=path, payload=payload) + return self.post(url, headers=headers, data=payload) + + # PRIVATE METHODS --------------------------------------------------------- + def _sign_payload(self, method, path, params=None, payload=None): + + route = build_route(path, params) + nonce = gen_nonce() + + if payload: + j = json.dumps(payload).encode('utf-8') + encoded_body = base64.standard_b64encode(j).decode('utf-8') + string = method + ' ' + route + ' ' + encoded_body + ' ' + nonce + else: + string = method + ' ' + route + ' ' + nonce + + h = hmac.new(key=self.SECRET.encode('utf-8'), + msg=string.encode('utf-8'), + digestmod=hashlib.sha384) + + signature = h.hexdigest() + + return { + 'X-SBTC-APIKEY': self.KEY, + 'X-SBTC-NONCE': nonce, + 'X-SBTC-SIGNATURE': signature, + 'Content-Type': 'application/json', + } diff --git a/surbtc/client_public.py b/surbtc/client_public.py new file mode 100644 index 0000000..e1f8ee3 --- /dev/null +++ b/surbtc/client_public.py @@ -0,0 +1,38 @@ +# local +from .base import Client + +from . import constants as _c +from . import models as _m +from .server import SURBTCServer + +_p = _c.Path + + +class SURBTCPublic(Client): + + def __init__(self, test=False, timeout=30): + Client.__init__(self, SURBTCServer(test), timeout) + + def markets(self): + url, path = self.url_path_for(_p.MARKETS) + data = self.get(url) + return [_m.Market.create_from_json(market) + for market in data['markets']] + + def market_details(self, market_id: _c.Market): + market_id = _c.Market.check(market_id) + url = self.url_for(_p.MARKET_DETAILS, path_arg=market_id.value) + data = self.get(url) + return _m.Market.create_from_json(data['market']) + + def ticker(self, market_id: _c.Market): + market_id = _c.Market.check(market_id) + url = self.url_for(_p.TICKER, path_arg=market_id.value) + data = self.get(url) + return _m.Ticker.create_from_json(data['ticker']) + + def order_book(self, market_id: _c.Market): + market_id = _c.Market.check(market_id) + url = self.url_for(_p.ORDER_BOOK, path_arg=market_id.value) + data = self.get(url) + return _m.OrderBook.create_from_json(data['order_book']) diff --git a/surbtc/common.py b/surbtc/common.py index b13611a..4352f82 100644 --- a/surbtc/common.py +++ b/surbtc/common.py @@ -1,6 +1,7 @@ -from urllib.parse import urlencode import logging import time +from urllib.parse import urlencode + # pip import requests @@ -76,4 +77,4 @@ def update_dictionary(old_dict: dict, new_dict: dict): if new_dict: keys = list(new_dict.keys()) for k in keys: - old_dict[k] = new_dict[k] + old_dict[k] = new_dict[k] \ No newline at end of file diff --git a/surbtc/constants.py b/surbtc/constants.py new file mode 100644 index 0000000..5b9e525 --- /dev/null +++ b/surbtc/constants.py @@ -0,0 +1,93 @@ +from enum import Enum + +# Limits +ORDERS_LIMIT = 300 + + +# API paths +class Path(object): + MARKETS = 'markets' + MARKET_DETAILS = 'markets/%s' + TICKER = "markets/%s/ticker" + ORDER_BOOK = 'markets/%s/order_book' + QUOTATION = 'markets/%s/quotations' + FEE_PERCENTAGE = 'markets/%s/fee_percentage' + TRADE_TRANSACTIONS = 'markets/%s/trade_transactions' + REPORTS = 'markets/%s/reports' + BALANCES = 'balances/%s' + BALANCES_EVENTS = 'balance_events' + ORDERS = 'markets/%s/orders' + SINGLE_ORDER = 'orders/%s' + WITHDRAWAL = 'currencies/%s/withdrawals' + + +class _Enum(Enum): + + @staticmethod + def _format_value(value): + return str(value).upper() + + @classmethod + def check(cls, value): + if value is None: + return value + if type(value) is cls: + return value + try: + return cls[cls._format_value(value)] + except KeyError: + return cls._missing_(value) + + +class Currency(_Enum): + BTC = 'BTC' + CLP = 'CLP' + COP = 'COP' + + +class Market(_Enum): + BTC_CLP = 'BTC-CLP' + BTC_COP = 'BTC-COP' + + @staticmethod + def _format_value(value): + return str(value).replace('-', '_').upper() + + +class QuotationType(_Enum): + BID_GIVEN_SIZE = 'bid_given_size' + BID_GIVEN_EARNED_BASE = 'bid_given_earned_base' + BID_GIVEN_SPENT_QUOTE = 'bid_given_spent_quote' + ASK_GIVEN_SIZE = 'ask_given_size' + ASK_GIVEN_EARNED_QUOTE = 'ask_given_earned_quote' + ASK_GIVEN_SPENT_BASE = 'ask_given_spent_base' + + +class OrderType(_Enum): + ASK = 'Ask' + BID = 'Bid' + + +class OrderPriceType(_Enum): + MARKET = 'market' + LIMIT = 'limit' + + +class OrderState(_Enum): + RECEIVED = 'received' + PENDING = 'pending' + TRADED = 'traded' + CANCELING = 'canceling' + CANCELED = 'canceled' + + +class BalanceEvent(_Enum): + DEPOSIT_CONFIRM = 'deposit_confirm' + WITHDRAWAL_CONFIRM = 'withdrawal_confirm' + TRANSACTION = 'transaction' + TRANSFER_CONFIRMATION = 'transfer_confirmation' + + +class ReportType(_Enum): + CANDLESTICK = 'candlestick' + AVERAGE_PRICES = 'average_prices' diff --git a/surbtc/models.py b/surbtc/models.py new file mode 100644 index 0000000..5439eca --- /dev/null +++ b/surbtc/models.py @@ -0,0 +1,341 @@ +from collections import namedtuple +from datetime import datetime +import math + + +def parse_datetime(datetime_str): + return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S.%fZ') + + +class Amount( + namedtuple('amount', [ + 'amount', + 'currency', + ]) +): + @classmethod + def create_from_json(cls, amount): + if amount: + amount = cls( + amount=float(amount[0]), + currency=amount[1], + ) + return amount + + +class PagesMeta( + namedtuple('meta', [ + 'current_page', + 'total_count', + 'total_pages', + ]) +): + @classmethod + def create_from_json(cls, meta): + return cls( + current_page=meta['current_page'], + total_count=meta['total_count'], + total_pages=meta['total_pages'] + ) + + +class Market( + namedtuple('market', [ + 'id', + 'name', + 'base_currency', + 'quote_currency', + 'minimum_order_amount', + ]), +): + @classmethod + def create_from_json(cls, market): + return cls( + id=market['id'], + name=market['name'], + base_currency=market['base_currency'], + quote_currency=market['quote_currency'], + minimum_order_amount=Amount.create_from_json( + market['minimum_order_amount'] + ), + ) + + +class Ticker( + namedtuple('ticker', [ + 'last_price', + 'min_ask', + 'max_bid', + 'volume', + 'price_variation_24h', + 'price_variation_7d' + ]) +): + @classmethod + def create_from_json(cls, ticker): + return cls( + last_price=Amount.create_from_json(ticker['last_price']), + min_ask=Amount.create_from_json(ticker['min_ask']), + max_bid=Amount.create_from_json(ticker['max_bid']), + volume=Amount.create_from_json(ticker['volume']), + price_variation_24h=float(ticker['price_variation_24h']), + price_variation_7d=float(ticker['price_variation_7d']), + ) + + +class Quotation( + namedtuple('quotation', [ + 'amount', + 'base_balance_change', + 'base_exchanged', + 'fee', + 'incomplete', + 'limit', + 'order_amount', + 'quote_balance_change', + 'quote_exchanged', + 'type', + ]) +): + @classmethod + def create_from_json(cls, quotation): + return cls( + amount=Amount.create_from_json( + quotation['amount']), + base_balance_change=Amount.create_from_json( + quotation['base_balance_change']), + base_exchanged=Amount.create_from_json( + quotation['base_exchanged']), + fee=Amount.create_from_json( + quotation['fee']), + incomplete=quotation['incomplete'], + limit=Amount.create_from_json( + quotation['limit']), + order_amount=Amount.create_from_json( + quotation['order_amount']), + quote_balance_change=Amount.create_from_json( + quotation['quote_balance_change']), + quote_exchanged=Amount.create_from_json( + quotation['quote_exchanged']), + type=quotation['type'], + ) + + +class OrderBookEntry( + namedtuple('book_entry', [ + 'price', + 'amount', + ]) +): + @classmethod + def create_from_json(cls, book_entry): + return cls( + price=float(book_entry[0]), + amount=float(book_entry[1]), + ) + + +class OrderBook( + namedtuple('order_book', [ + 'asks', + 'bids', + ]) +): + @classmethod + def create_from_json(cls, order_book): + return cls( + asks=[OrderBookEntry.create_from_json(entry) + for entry in order_book['asks']], + bids=[OrderBookEntry.create_from_json(entry) + for entry in order_book['bids']], + ) + + +class FeePercentage( + namedtuple('fee_percentage', [ + 'value', + ]) +): + @classmethod + def create_from_json(cls, fee_percentage): + return cls( + value=float(fee_percentage['value']), + ) + + +class Balance( + namedtuple('balance', [ + 'id', + 'account_id', + 'amount', + 'available_amount', + 'frozen_amount', + 'pending_withdraw_amount', + ]) +): + @classmethod + def create_from_json(cls, balance): + return cls( + id=balance['id'], + account_id=balance['account_id'], + amount=Amount.create_from_json( + balance['amount']), + available_amount=Amount.create_from_json( + balance['available_amount']), + frozen_amount=Amount.create_from_json( + balance['frozen_amount']), + pending_withdraw_amount=Amount.create_from_json( + balance['pending_withdraw_amount']), + ) + + +class Order( + namedtuple('order', [ + 'id', + 'account_id', + 'amount', + 'created_at', + 'fee_currency', + 'limit', + 'market_id', + 'original_amount', + 'paid_fee', + 'price_type', + 'state', + 'total_exchanged', + 'traded_amount', + 'type', + 'weighted_quotation', + ]) +): + @classmethod + def create_from_json(cls, order): + return cls( + id=order['id'], + account_id=order['account_id'], + amount=Amount.create_from_json(order['amount']), + created_at=parse_datetime(order['created_at']), + fee_currency=order['fee_currency'], + limit=Amount.create_from_json(order['limit']), + market_id=order['market_id'], + original_amount=Amount.create_from_json(order['original_amount']), + paid_fee=Amount.create_from_json(order['paid_fee']), + price_type=order['price_type'], + state=order['state'], + total_exchanged=Amount.create_from_json(order['total_exchanged']), + traded_amount=Amount.create_from_json(order['traded_amount']), + type=order['type'], + weighted_quotation=order['weighted_quotation'], + ) + + +class OrderPages( + namedtuple('order_pages', [ + 'orders', + 'meta', + ]) +): + @classmethod + def create_from_json(cls, orders, pages_meta): + return cls( + orders=[Order.create_from_json(order) + for order in orders], + meta=PagesMeta.create_from_json(pages_meta), + ) + + +class BalanceEvent( + namedtuple('balance_event', [ + 'id', + 'account_id', + 'created_at', + 'currency', + 'event', + 'event_ids', + 'new_amount', + 'new_available_amount', + 'new_frozen_amount', + 'new_frozen_for_fee', + 'new_pending_withdraw_amount', + 'old_amount', + 'old_available_amount', + 'old_frozen_amount', + 'old_frozen_for_fee', + 'old_pending_withdraw_amount', + 'transaction_type', + 'transfer_description', + ]) +): + @classmethod + def create_from_json(cls, event): + return cls( + id=event['id'], + account_id=event['account_id'], + created_at=parse_datetime(event['created_at']), + currency=event['currency'], + event=event['event'], + event_ids=event['event_ids'], + new_amount=event['new_amount'], + new_available_amount=event['new_available_amount'], + new_frozen_amount=event['new_frozen_amount'], + new_frozen_for_fee=event['new_frozen_for_fee'], + new_pending_withdraw_amount=event['new_pending_withdraw_amount'], + old_amount=event['old_amount'], + old_available_amount=event['old_available_amount'], + old_frozen_amount=event['old_frozen_amount'], + old_frozen_for_fee=event['old_frozen_for_fee'], + old_pending_withdraw_amount=event['old_pending_withdraw_amount'], + transaction_type=event['transaction_type'], + transfer_description=event['transfer_description'], + ) + + +class BalanceEventPages( + namedtuple('event_pages', [ + 'balance_events', + 'meta', + ]) +): + @classmethod + def create_from_json(cls, events, total_count, page): + return cls( + balance_events=[BalanceEvent.create_from_json(event) + for event in events], + meta=PagesMeta( + current_page=page or 1, + total_count=total_count, + total_pages=math.ceil(total_count / len(events))), + ) + + +class TradeTransaction( + namedtuple('trade_transaction', [ + 'id', + 'market_id', + 'created_at', + 'updated_at', + 'amount_sold', + 'price_paid', + 'ask_order', + 'bid_order', + 'triggering_order', + ]) +): + @classmethod + def create_from_json(cls, transaction): + return cls( + id=transaction['id'], + market_id=transaction['market_id'], + created_at=parse_datetime(transaction['created_at']), + updated_at=parse_datetime(transaction['updated_at']), + amount_sold=Amount.create_from_json( + [transaction['amount_sold'], + transaction['amount_sold_currency']]), + price_paid=Amount.create_from_json( + [transaction['price_paid'], + transaction['price_paid_currency']]), + ask_order=Order.create_from_json(transaction['ask']), + bid_order=Order.create_from_json(transaction['bid']), + triggering_order=Order.create_from_json( + transaction['triggering_order']), + ) diff --git a/surbtc/server.py b/surbtc/server.py new file mode 100644 index 0000000..3084265 --- /dev/null +++ b/surbtc/server.py @@ -0,0 +1,15 @@ +from .base import Server + +# API Server +PROTOCOL = 'https' +HOST = 'www.surbtc.com/api' +TEST_HOST = 'stg.surbtc.com/api' +VERSION = 'v2' + + +# SURBTC API server +class SURBTCServer(Server): + + def __init__(self, test): + host = HOST if not test else TEST_HOST + Server.__init__(self, PROTOCOL, host, VERSION) \ No newline at end of file diff --git a/test/surbtc_test.py b/test/surbtc_test.py index 445abeb..72b2496 100644 --- a/test/surbtc_test.py +++ b/test/surbtc_test.py @@ -1,96 +1,128 @@ import unittest + +from datetime import datetime, timedelta + # pip from decouple import config from requests import RequestException + # local from surbtc import SURBTC +from surbtc import models TEST = config('TEST', cast=bool, default=False) API_KEY = config('SURBTC_API_KEY') API_SECRET = config('SURBTC_API_SECRET') -MARKET_ID = 'btc-clp' +MARKET_ID = SURBTC.Market.BTC_CLP -class SURBTCTest(unittest.TestCase): +class SURBTCPublicTest(unittest.TestCase): def setUp(self): - self.client = SURBTC(API_KEY, API_SECRET, TEST) + self.client = SURBTC.Public(TEST) def test_instantiate_client(self): - self.assertIsInstance(self.client, SURBTC) + self.assertIsInstance(self.client, SURBTC.Public) - def test_markets_returns_data(self): + def test_markets(self): markets = self.client.markets() - self.assertIn('markets', markets.keys()) + self.assertEqual(len(markets), len(SURBTC.Market)) + for market in markets: + self.assertIsInstance(market, models.Market) - def test_markets_details_returns_data(self): - markets_details = self.client.market_details(MARKET_ID) - self.assertIn('market', markets_details.keys()) + def test_markets_details(self): + market = self.client.market_details(MARKET_ID) + self.assertIsInstance(market, models.Market) - def test_indicators_returns_data(self): - indicators = self.client.indicators(MARKET_ID) - self.assertIn('indicators', indicators.keys()) + def test_ticker(self): + ticker = self.client.ticker(MARKET_ID) + self.assertIsInstance(ticker, models.Ticker) - def test_order_book_returns_data(self): + def test_order_book(self): order_book = self.client.order_book(MARKET_ID) - self.assertIn('order_book', order_book.keys()) + self.assertIsInstance(order_book, models.OrderBook) + + +class SURBTCAuthTest(unittest.TestCase): + + def setUp(self): + self.client = SURBTC.Auth(API_KEY, API_SECRET, TEST) - def test_quotation_returns_data(self): + def test_instantiate_client(self): + self.assertIsInstance(self.client, SURBTC.Auth) + + def test_quotation(self): quotation = self.client.quotation( - MARKET_ID, quotation_type='ask', reverse=False, amount=1) - self.assertIn('quotation', quotation.keys()) + MARKET_ID, SURBTC.Currency.BTC, + SURBTC.QuotationType.ASK_GIVEN_SIZE, + price_limit=1, amount=1) + self.assertIsInstance(quotation, models.Quotation) - def test_fee_percentage_returns_data(self): + def test_fee_percentage(self): fee_percentage = self.client.fee_percentage( - MARKET_ID, order_type='Bid', market_order=False) - self.assertIn('fee_percentage', fee_percentage.keys()) + MARKET_ID, SURBTC.OrderType.ASK, market_order=False) + self.assertIsInstance(fee_percentage, models.FeePercentage) + + def test_trade_transaction_pages(self): + trade_transactions = self.client.trade_transaction_pages(MARKET_ID) + for transaction in trade_transactions: + self.assertIsInstance(transaction, models.TradeTransaction) + + def test_report(self): + end = datetime.now() + start = end - timedelta(days=30) + reports = self.client.report( + MARKET_ID, SURBTC.ReportType.CANDLESTICK, start, end) + self.assertIn('reports', reports.keys()) - def test_trade_transactions_returns_data(self): - trade_transactions = self.client.trade_transactions(MARKET_ID) - self.assertIn('trade_transactions', trade_transactions.keys()) + def test_balance(self): + balance = self.client.balance(SURBTC.Currency.BTC) + self.assertIsInstance(balance, models.Balance) - def test_reports_returns_data(self): - reports = self.client.reports(MARKET_ID, report_type='candlestick') - self.assertIn('reports', reports.keys()) + def test_balances_event_pages(self): + currencies = [item for item in SURBTC.Currency] + event_names = [item for item in SURBTC.BalanceEvent] + balance_events = self.client.balance_event_pages( + currencies, event_names) + self.assertIsInstance(balance_events, models.BalanceEventPages) - def test_balance_returns_data(self): - balance = self.client.balance(currency='BTC') - self.assertIn('balance', balance.keys()) - - def test_balances_events_returns_data(self): - currencies = ['BTC', 'CLP'] - event_names = [ - 'deposit_confirm', - 'withdrawal_confirm', - 'transaction', - 'transfer_confirmation', - ] - balance_events = self.client.balance_events(currencies, event_names) - self.assertIn('balance_events', balance_events.keys()) - - def test_orders_returns_data(self): + def test_order_pages(self): per_page = 10 - orders = self.client.orders(MARKET_ID, page=1, per_page=per_page) - self.assertIn('orders', orders.keys()) - self.assertEqual(len(orders['orders']), per_page) - - def test_single_order_returns_data(self): - orders = self.client.orders(MARKET_ID, page=1, per_page=1)['orders'] + order_pages = self.client.order_pages( + MARKET_ID, page=1, per_page=per_page) + self.assertIsInstance(order_pages, models.OrderPages) + self.assertEqual(len(order_pages.orders), per_page) + + def test_order_details(self): + orders = self.client.order_pages( + MARKET_ID, page=1, per_page=1).orders first_order = orders[0] - single_order = self.client.single_order(first_order['id']) - self.assertIn('order', single_order.keys()) + single_order = self.client.order_details(first_order.id) + self.assertIsInstance(single_order, models.Order) + + @unittest.skipUnless(TEST, 'Only run on staging context') + def test_new_order_cancel_order(self): + # New order + new_order = self.client.new_order( + MARKET_ID, SURBTC.OrderType.ASK, SURBTC.OrderPriceType.LIMIT, + amount=0.001, limit=100000) + # Cancel order + canceled_order = self.client.cancel_order(new_order.id) + # Assertions + self.assertIsInstance(new_order, models.Order) + self.assertIsInstance(canceled_order, models.Order) -class SURBTCTestBadApi(unittest.TestCase): +class SURBTCAuthTestBadApi(unittest.TestCase): def setUp(self): - self.client = SURBTC('BAD_KEY', 'BAD_SECRET') + self.client = SURBTC.Auth('BAD_KEY', 'BAD_SECRET') def test_instantiate_client(self): - self.assertIsInstance(self.client, SURBTC) + self.assertIsInstance(self.client, SURBTC.Auth) def test_key_secret(self): - self.assertRaises(ValueError, lambda: SURBTC()) + self.assertRaises(ValueError, lambda: SURBTC.Auth()) - def test_markets_returns_error(self): - self.assertRaises(RequestException, lambda: self.client.markets()) + def test_balance_returns_error(self): + self.assertRaises(RequestException, lambda: self.client.balance('clp'))