├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── requierements.txt ├── setup.cfg ├── setup.py ├── surbtc ├── __init__.py ├── base.py ├── client_auth.py ├── client_public.py ├── common.py ├── constants.py ├── errors.py ├── logger.py ├── models.py └── server.py ├── test ├── __init__.py └── surbtc_test.py └── tox.ini /.env.example: -------------------------------------------------------------------------------- 1 | # Don't put quotes around your keys! 2 | 3 | TEST=False 4 | 5 | # SURBTC 6 | SURBTC_API_KEY=XXXXXXXX 7 | SURBTC_API_SECRET=XXXXXXXX 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/pycharm,visualstudiocode,python 3 | 4 | ### PyCharm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # Ignore all the .idea folder 9 | .idea/* 10 | 11 | # User-specific stuff: 12 | .idea/workspace.xml 13 | .idea/tasks.xml 14 | 15 | # Sensitive or high-churn files: 16 | .idea/dataSources/ 17 | .idea/dataSources.ids 18 | .idea/dataSources.xml 19 | .idea/dataSources.local.xml 20 | .idea/sqlDataSources.xml 21 | .idea/dynamic.xml 22 | .idea/uiDesigner.xml 23 | 24 | # Gradle: 25 | .idea/gradle.xml 26 | .idea/libraries 27 | 28 | # Mongo Explorer plugin: 29 | .idea/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | 51 | ### PyCharm Patch ### 52 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 53 | 54 | # *.iml 55 | # modules.xml 56 | # .idea/misc.xml 57 | # *.ipr 58 | 59 | 60 | ### VisualStudioCode ### 61 | .vscode/* 62 | !.vscode/settings.json 63 | !.vscode/tasks.json 64 | !.vscode/launch.json 65 | !.vscode/extensions.json 66 | 67 | 68 | ### Python ### 69 | # Byte-compiled / optimized / DLL files 70 | __pycache__/ 71 | *.py[cod] 72 | *$py.class 73 | 74 | # C extensions 75 | *.so 76 | 77 | # Distribution / packaging 78 | .Python 79 | env/ 80 | build/ 81 | develop-eggs/ 82 | dist/ 83 | downloads/ 84 | eggs/ 85 | .eggs/ 86 | lib/ 87 | lib64/ 88 | parts/ 89 | sdist/ 90 | var/ 91 | *.egg-info/ 92 | .installed.cfg 93 | *.egg 94 | 95 | # PyInstaller 96 | # Usually these files are written by a python script from a template 97 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 98 | *.manifest 99 | *.spec 100 | 101 | # Installer logs 102 | pip-log.txt 103 | pip-delete-this-directory.txt 104 | 105 | # Unit test / coverage reports 106 | htmlcov/ 107 | .tox/ 108 | .coverage 109 | .coverage.* 110 | .cache 111 | nosetests.xml 112 | coverage.xml 113 | *,cover 114 | .hypothesis/ 115 | 116 | # Translations 117 | *.mo 118 | *.pot 119 | 120 | # Django stuff: 121 | *.log 122 | local_settings.py 123 | 124 | # Flask stuff: 125 | instance/ 126 | .webassets-cache 127 | 128 | # Scrapy stuff: 129 | .scrapy 130 | 131 | # Sphinx documentation 132 | docs/_build/ 133 | 134 | # PyBuilder 135 | target/ 136 | 137 | # Jupyter Notebook 138 | .ipynb_checkpoints 139 | 140 | # pyenv 141 | .python-version 142 | 143 | # celery beat schedule file 144 | celerybeat-schedule 145 | 146 | # dotenv 147 | .env 148 | 149 | # virtualenv 150 | .venv/ 151 | venv/ 152 | ENV/ 153 | 154 | # Spyder project settings 155 | .spyderproject 156 | 157 | # Rope project settings 158 | .ropeproject 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | Felipe Aránguiz 5 | Sebastián Aránguiz 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python SURBTC API Wrapper 2 | 3 | SURBTC Python API Wrapper, Cryptocurrency Exchange for Chile, Colombia and Peru. 4 | Tested on Python 3.5 5 | 6 | [Go to SURBTC](https://www.surbtc.com) 7 | 8 | ## Dev Setup 9 | 10 | Install the libs 11 | 12 | pip install -r requirements.txt 13 | 14 | Rename .env.example > .env 15 | 16 | ## Installation 17 | 18 | pip install git+https://github.com/delta575/python-surbtc-api.git 19 | 20 | ## Usage 21 | 22 | ### Setup Public: 23 | 24 | from surbtc import SURBTC 25 | client = SURBTC.Public() 26 | 27 | ### Setup Auth (ApiKey/Secret requiered, Test is optional (default: False)): 28 | 29 | from surbtc import SURBTC 30 | client = SURBTC.Auth(API_KEY, API_SECRET, TEST) 31 | 32 | ## Market Pairs: 33 | 34 | ### Bitcoin Pairs: 35 | btc-clp 36 | btc-cop 37 | btc-pen 38 | 39 | ### Ether Pairs: 40 | eth-btc 41 | eth-clp 42 | eth-cop 43 | eth-pen 44 | 45 | Open for everyone: 46 | [www.surbtc.com](https://www.surbtc.com) 47 | 48 | SURBTC API Docs: 49 | [www.surbtc.com/docs/api](https://api.surbtc.com) 50 | 51 | ## Licence 52 | 53 | The MIT License (MIT) 54 | 55 | Copyright (c) 2016 Felipe Aránguiz | Sebastián Aránguiz 56 | 57 | See [LICENSE](LICENSE) 58 | 59 | ## Based on 60 | 61 | [scottjbarr/bitfinex](https://github.com/scottjbarr/bitfinex) 62 | -------------------------------------------------------------------------------- /requierements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.4.1 2 | pytest==3.2.0 3 | python-decouple==3.0 4 | requests==2.18.3 5 | tox==2.7.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | #H301 — one import per line 3 | #H306 — imports not in alphabetical order 4 | ignore = H301,H306 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup 3 | 4 | if not (sys.version_info >= (3, 5)): 5 | sys.exit('Sorry, only Python 3.5 or later is supported') 6 | 7 | setup( 8 | name='surbtc', 9 | version='0.2.4', 10 | description='SURBTC API Wrapper for Python 3', 11 | url='https://github.com/delta575/python-surbtc-api', 12 | author='Felipe Aránguiz, Sebastian Aránguiz', 13 | authoremail='faranguiz575@gmail.com, sarang575@gmail.com', 14 | license='MIT', 15 | packages=[ 16 | 'surbtc' 17 | ], 18 | package_dir={ 19 | 'surbtc': 'surbtc', 20 | }, 21 | install_requires=[ 22 | 'requests', 23 | ], 24 | tests_require=[ 25 | 'python-decouple', 26 | ], 27 | zip_safe=True 28 | ) 29 | -------------------------------------------------------------------------------- /surbtc/__init__.py: -------------------------------------------------------------------------------- 1 | from . import constants as _c 2 | from . import models as _m 3 | from .client_auth import SURBTCAuth 4 | from .client_public import SURBTCPublic 5 | 6 | 7 | class SURBTC(object): 8 | # Models 9 | models = _m 10 | # Enum Types 11 | BalanceEvent = _c.BalanceEvent 12 | Currency = _c.Currency 13 | Market = _c.Market 14 | OrderState = _c.OrderState 15 | OrderType = _c.OrderType 16 | OrderPriceType = _c.OrderPriceType 17 | QuotationType = _c.QuotationType 18 | ReportType = _c.ReportType 19 | # Clients 20 | Auth = SURBTCAuth 21 | Public = SURBTCPublic 22 | 23 | 24 | __all__ = [ 25 | SURBTC, 26 | ] 27 | -------------------------------------------------------------------------------- /surbtc/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | from enum import Enum 3 | from urllib.parse import urlparse 4 | 5 | # pip 6 | import requests 7 | 8 | # local 9 | from . import errors 10 | 11 | 12 | class Server(object): 13 | 14 | def __init__(self, protocol, host, version=None): 15 | url = '{0:s}://{1:s}'.format(protocol, host) 16 | if version: 17 | url = '{0:s}/{1:s}'.format(url, version) 18 | 19 | self.PROTOCOL = protocol 20 | self.HOST = host 21 | self.VERSION = version 22 | self.URL = url 23 | 24 | 25 | class Client(object): 26 | 27 | error_key = '' 28 | 29 | def __init__(self, server: Server, timeout=30): 30 | self.SERVER = server 31 | self.TIMEOUT = timeout 32 | 33 | def get(self, url, headers=None, params=None): 34 | response = self._request('get', url, headers=headers, params=params) 35 | return response 36 | 37 | def put(self, url, headers, data): 38 | response = self._request('put', url, headers=headers, data=data) 39 | return response 40 | 41 | def post(self, url, headers, data): 42 | response = self._request('post', url, headers=headers, data=data) 43 | return response 44 | 45 | def _request(self, method, url, headers, params=None, data=None): 46 | data = self._encode_data(data) 47 | response = requests.request( 48 | method, 49 | url, 50 | headers=headers, 51 | params=params, 52 | data=data, 53 | verify=True, 54 | timeout=self.TIMEOUT) 55 | json_resp = self._resp_to_json(response) 56 | self._check_response(response, json_resp) 57 | return json_resp 58 | 59 | def _encode_data(self, data): 60 | data = json.dumps(data) if data else data 61 | return data 62 | 63 | def _check_response(self, response: requests.Response, message: dict): 64 | try: 65 | has_error = bool(message.get(self.error_key)) 66 | except AttributeError: 67 | has_error = False 68 | try: 69 | response.raise_for_status() 70 | except requests.exceptions.HTTPError as e: 71 | raise errors.InvalidResponse(response) from e 72 | if has_error: 73 | raise errors.InvalidResponse(response) 74 | 75 | def _resp_to_json(self, response): 76 | try: 77 | json_resp = response.json() 78 | except json.decoder.JSONDecodeError as e: 79 | raise errors.DecodeError() from e 80 | return json_resp 81 | 82 | def url_for(self, path, path_arg=None): 83 | url = '{0:s}/{1:s}'.format(self.SERVER.URL, path) 84 | if path_arg: 85 | url = url % path_arg 86 | return url 87 | 88 | def url_path_for(self, path, path_arg=None): 89 | url = self.url_for(path, path_arg) 90 | path = urlparse(url).path 91 | return url, path 92 | 93 | 94 | class _Enum(Enum): 95 | @staticmethod 96 | def _format_value(value): 97 | return str(value).upper() 98 | 99 | @classmethod 100 | def check(cls, value): 101 | if value is None: 102 | return value 103 | if type(value) is cls: 104 | return value 105 | try: 106 | return cls[cls._format_value(value)] 107 | except KeyError: 108 | return cls._missing_(value) 109 | -------------------------------------------------------------------------------- /surbtc/client_auth.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import hmac 4 | import json 5 | from datetime import datetime 6 | 7 | # local 8 | from . import constants as _c 9 | from . import models as _m 10 | from .common import build_route, check_keys, gen_nonce 11 | from .client_public import SURBTCPublic 12 | 13 | _p = _c.Path 14 | 15 | 16 | class SURBTCAuth(SURBTCPublic): 17 | 18 | def __init__(self, key=False, secret=False, test=False, timeout=30): 19 | SURBTCPublic.__init__(self, test, timeout) 20 | check_keys(key, secret) 21 | self.KEY = str(key) 22 | self.SECRET = str(secret) 23 | 24 | def quotation(self, 25 | market_id: _c.Market, 26 | quotation_type: _c.QuotationType, 27 | amount: float, 28 | limit: float=None): 29 | market_id = _c.Market.check(market_id).value 30 | quotation_type = _c.QuotationType.check(quotation_type).value 31 | payload = { 32 | 'quotation': { 33 | 'type': quotation_type, 34 | 'amount': str(amount), 35 | 'limit': str(limit) if limit else None, 36 | }, 37 | } 38 | url, path = self.url_path_for(_p.QUOTATION, 39 | path_arg=market_id) 40 | headers = self._sign_payload(method='POST', path=path, payload=payload) 41 | data = self.post(url, headers=headers, data=payload) 42 | return _m.Quotation.create_from_json(data['quotation']) 43 | 44 | def quotation_market(self, 45 | market_id: _c.Market, 46 | quotation_type: _c.QuotationType, 47 | amount: float): 48 | return self.quotation( 49 | market_id=market_id, quotation_type=quotation_type, 50 | amount=amount, limit=None) 51 | 52 | def quotation_limit(self, 53 | market_id: _c.Market, 54 | quotation_type: _c.QuotationType, 55 | amount: float, 56 | limit: float): 57 | return self.quotation( 58 | market_id=market_id, quotation_type=quotation_type, 59 | amount=amount, limit=limit) 60 | 61 | # REPORTS ----------------------------------------------------------------- 62 | def _report(self, 63 | market_id: _c.Market, 64 | report_type: _c.ReportType, 65 | start_at: datetime=None, 66 | end_at: datetime=None): 67 | market_id = _c.Market.check(market_id).value 68 | report_type = _c.ReportType.check(report_type).value 69 | if isinstance(start_at, datetime): 70 | start_at = int(start_at.timestamp()) 71 | if isinstance(end_at, datetime): 72 | end_at = int(end_at.timestamp()) 73 | params = { 74 | 'report_type': report_type, 75 | 'from': start_at, 76 | 'to': end_at, 77 | } 78 | url, path = self.url_path_for(_p.REPORTS, path_arg=market_id) 79 | headers = self._sign_payload(method='GET', path=path, params=params) 80 | data = self.get(url, headers=headers, params=params) 81 | return data 82 | 83 | def report_average_prices(self, 84 | market_id: _c.Market, 85 | start_at: datetime = None, 86 | end_at: datetime = None): 87 | data = self._report( 88 | market_id=market_id, report_type=_c.ReportType.AVERAGE_PRICES, 89 | start_at=start_at, end_at=end_at) 90 | return [_m.AveragePrice.create_from_json(report) 91 | for report in data['reports']] 92 | 93 | def report_candlestick(self, 94 | market_id: _c.Market, 95 | start_at: datetime = None, 96 | end_at: datetime = None): 97 | data = self._report( 98 | market_id=market_id, report_type=_c.ReportType.CANDLESTICK, 99 | start_at=start_at, end_at=end_at) 100 | return [_m.Candlestick.create_from_json(report) 101 | for report in data['reports']] 102 | 103 | # BALANCES----------------------------------------------------------------- 104 | def balance(self, currency: _c.Currency): 105 | currency = _c.Currency.check(currency) 106 | url, path = self.url_path_for(_p.BALANCES, path_arg=currency.value) 107 | headers = self._sign_payload(method='GET', path=path) 108 | data = self.get(url, headers=headers) 109 | return _m.Balance.create_from_json(data['balance']) 110 | 111 | def balance_event_pages(self, 112 | currencies, 113 | event_names, 114 | page: int=None, 115 | per_page: int=None, 116 | relevant: bool=None): 117 | currencies = [_c.Currency.check(c).value 118 | for c in currencies] 119 | event_names = [_c.BalanceEvent.check(e).value 120 | for e in event_names] 121 | params = { 122 | 'currencies[]': currencies, 123 | 'event_names[]': event_names, 124 | 'page': page, 125 | 'per': per_page, 126 | 'relevant': relevant, 127 | } 128 | url, path = self.url_path_for(_p.BALANCES_EVENTS) 129 | headers = self._sign_payload(method='GET', path=path, params=params) 130 | data = self.get(url, headers=headers, params=params) 131 | # TODO: Response only contains a 'total_count' field instead of meta 132 | return _m.BalanceEventPages.create_from_json( 133 | data['balance_events'], data['total_count'], page) 134 | 135 | # ORDERS ------------------------------------------------------------------ 136 | def new_order(self, 137 | market_id: _c.Market, 138 | order_type: _c.OrderType, 139 | price_type: _c.OrderPriceType, 140 | amount: float, 141 | limit: float=None): 142 | market_id = _c.Market.check(market_id) 143 | order_type = _c.OrderType.check(order_type) 144 | price_type = _c.OrderPriceType.check(price_type) 145 | payload = { 146 | 'type': order_type.value, 147 | 'price_type': price_type.value, 148 | 'amount': str(amount), 149 | 'limit': str(limit) if limit else None, 150 | } 151 | return self.new_order_payload(market_id, payload) 152 | 153 | def new_order_payload(self, market_id: _c.Market, payload): 154 | market_id = _c.Market.check(market_id) 155 | url, path = self.url_path_for(_p.ORDERS, path_arg=market_id.value) 156 | headers = self._sign_payload(method='POST', path=path, payload=payload) 157 | data = self.post(url, headers=headers, data=payload) 158 | return _m.Order.create_from_json(data['order']) 159 | 160 | def order_pages(self, 161 | market_id: _c.Market, 162 | page: int=None, 163 | per_page: int=None, 164 | state: _c.OrderState=None, 165 | minimum_exchanged: float=None): 166 | market_id = _c.Market.check(market_id) 167 | state = _c.OrderState.check(state) 168 | if per_page and per_page > _c.ORDERS_LIMIT: 169 | msg = "Param 'per_page' must be <= {0}".format(_c.ORDERS_LIMIT) 170 | raise ValueError(msg) 171 | params = { 172 | 'per': per_page, 173 | 'page': page, 174 | 'state': state.value if state else state, 175 | 'minimum_exchanged': minimum_exchanged, 176 | } 177 | url, path = self.url_path_for(_p.ORDERS, 178 | path_arg=market_id.value) 179 | headers = self._sign_payload(method='GET', path=path, params=params) 180 | data = self.get(url, headers=headers, params=params) 181 | return _m.OrderPages.create_from_json(data['orders'], data.get('meta')) 182 | 183 | def order_details(self, order_id: int): 184 | url, path = self.url_path_for(_p.SINGLE_ORDER, 185 | path_arg=order_id) 186 | headers = self._sign_payload(method='GET', path=path) 187 | data = self.get(url, headers=headers) 188 | return _m.Order.create_from_json(data['order']) 189 | 190 | def cancel_order(self, order_id: int): 191 | payload = { 192 | 'state': _c.OrderState.CANCELING.value, 193 | } 194 | url, path = self.url_path_for(_p.SINGLE_ORDER, 195 | path_arg=order_id) 196 | headers = self._sign_payload(method='PUT', path=path, payload=payload) 197 | data = self.put(url, headers=headers, data=payload) 198 | return _m.Order.create_from_json(data['order']) 199 | 200 | # PAYMENTS ---------------------------------------------------------------- 201 | def _transfers(self, currency: _c.Currency, path, model, key): 202 | currency = _c.Currency.check(currency).value 203 | url, path = self.url_path_for(path, path_arg=currency) 204 | headers = self._sign_payload(method='GET', path=path) 205 | data = self.get(url, headers=headers) 206 | return [model.create_from_json(transfer) 207 | for transfer in data[key]] 208 | 209 | def withdrawals(self, currency: _c.Currency): 210 | return self._transfers(currency=currency, path=_p.WITHDRAWALS, 211 | model=_m.Withdrawal, key='withdrawals') 212 | 213 | def deposits(self, currency: _c.Currency): 214 | return self._transfers(currency=currency, path=_p.DEPOSITS, 215 | model=_m.Deposit, key='deposits') 216 | 217 | # TODO: UNTESTED 218 | def withdrawal(self, 219 | currency: _c.Currency, 220 | amount: float, 221 | target_address, 222 | amount_includes_fee: bool=True, 223 | simulate: bool=False): 224 | currency = _c.Currency.check(currency).value 225 | payload = { 226 | 'withdrawal_data': { 227 | 'target_address': target_address, 228 | }, 229 | 'amount': str(amount), 230 | 'currency': currency, 231 | 'simulate': simulate, 232 | 'amount_includes_fee': amount_includes_fee, 233 | } 234 | url, path = self.url_path_for(_p.WITHDRAWALS, path_arg=currency) 235 | headers = self._sign_payload(method='POST', path=path, payload=payload) 236 | data = self.post(url, headers=headers, data=payload) 237 | return _m.Withdrawal.create_from_json(data['withdrawal']) 238 | 239 | def simulate_withdrawal(self, 240 | currency: _c.Currency, 241 | amount: float, 242 | amount_includes_fee: bool=True): 243 | return self.withdrawal( 244 | currency=currency, amount=amount, target_address=None, 245 | amount_includes_fee=amount_includes_fee, simulate=True) 246 | 247 | # PRIVATE METHODS --------------------------------------------------------- 248 | def _sign_payload(self, method, path, params=None, payload=None): 249 | 250 | route = build_route(path, params) 251 | nonce = gen_nonce() 252 | 253 | if payload: 254 | j = json.dumps(payload).encode('utf-8') 255 | encoded_body = base64.standard_b64encode(j).decode('utf-8') 256 | string = method + ' ' + route + ' ' + encoded_body + ' ' + nonce 257 | else: 258 | string = method + ' ' + route + ' ' + nonce 259 | 260 | h = hmac.new(key=self.SECRET.encode('utf-8'), 261 | msg=string.encode('utf-8'), 262 | digestmod=hashlib.sha384) 263 | 264 | signature = h.hexdigest() 265 | 266 | return { 267 | 'X-SBTC-APIKEY': self.KEY, 268 | 'X-SBTC-NONCE': nonce, 269 | 'X-SBTC-SIGNATURE': signature, 270 | 'Content-Type': 'application/json', 271 | } 272 | -------------------------------------------------------------------------------- /surbtc/client_public.py: -------------------------------------------------------------------------------- 1 | # local 2 | from . import constants as _c 3 | from . import models as _m 4 | from .base import Client 5 | from .server import SURBTCServer 6 | 7 | _p = _c.Path 8 | 9 | 10 | class SURBTCPublic(Client): 11 | 12 | error_key = 'message' 13 | 14 | def __init__(self, test=False, timeout=30): 15 | Client.__init__(self, SURBTCServer(test), timeout) 16 | 17 | def markets(self): 18 | url, path = self.url_path_for(_p.MARKETS) 19 | data = self.get(url) 20 | return [_m.Market.create_from_json(market) 21 | for market in data['markets']] 22 | 23 | def market_details(self, market_id: _c.Market): 24 | market_id = _c.Market.check(market_id) 25 | url = self.url_for(_p.MARKET_DETAILS, path_arg=market_id.value) 26 | data = self.get(url) 27 | return _m.Market.create_from_json(data['market']) 28 | 29 | def ticker(self, market_id: _c.Market): 30 | market_id = _c.Market.check(market_id) 31 | url = self.url_for(_p.TICKER, path_arg=market_id.value) 32 | data = self.get(url) 33 | return _m.Ticker.create_from_json(data['ticker']) 34 | 35 | def order_book(self, market_id: _c.Market): 36 | market_id = _c.Market.check(market_id) 37 | url = self.url_for(_p.ORDER_BOOK, path_arg=market_id.value) 38 | data = self.get(url) 39 | return _m.OrderBook.create_from_json(data['order_book']) 40 | 41 | def trade_transaction_pages(self, 42 | market_id: _c.Market, 43 | page: int=None, 44 | per_page: int=None): 45 | market_id = _c.Market.check(market_id) 46 | params = { 47 | 'page': page, 48 | 'per': per_page, 49 | } 50 | url, path = self.url_path_for(_p.TRADE_TRANSACTIONS, 51 | path_arg=market_id.value) 52 | data = self.get(url, params=params) 53 | return _m.TradeTransactionPages.create_from_json( 54 | data['trade_transactions'], data.get('meta')) 55 | -------------------------------------------------------------------------------- /surbtc/common.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta 3 | from urllib.parse import urlencode 4 | 5 | # local 6 | from . import logger 7 | 8 | 9 | def gen_nonce(): 10 | # Sleeps 200ms to avoid flooding the server with requests. 11 | time.sleep(0.2) 12 | # Get a str from the current time in microseconds. 13 | return str(int(time.time() * 1E6)) 14 | 15 | 16 | def check_keys(key, secret): 17 | if not key or not secret: 18 | msg = 'API Key and Secret are needed!' 19 | logger.log_error(msg) 20 | raise ValueError(msg) 21 | 22 | 23 | def build_parameters(parameters): 24 | if parameters: 25 | p = clean_parameters(parameters) 26 | return urlencode(p, True) 27 | else: 28 | return None 29 | 30 | 31 | def build_route(path, params=None): 32 | built_params = build_parameters(params) 33 | if built_params: 34 | return '{0:s}?{1:s}'.format(path, built_params) 35 | else: 36 | return path 37 | 38 | 39 | def clean_parameters(parameters: dict): 40 | if parameters: 41 | return {k: v for k, v in parameters.items() if v is not None} 42 | 43 | 44 | def update_dictionary(old_dict: dict, new_dict: dict): 45 | if new_dict: 46 | keys = list(new_dict.keys()) 47 | for k in keys: 48 | old_dict[k] = new_dict[k] 49 | 50 | 51 | def date_range(start_date, end_date): 52 | for n in range(int((end_date - start_date).days)): 53 | yield start_date + timedelta(n) 54 | 55 | 56 | def current_utc_date(): 57 | return datetime.utcnow().date() 58 | -------------------------------------------------------------------------------- /surbtc/constants.py: -------------------------------------------------------------------------------- 1 | from .base import _Enum 2 | 3 | # Limits 4 | ORDERS_LIMIT = 300 5 | 6 | 7 | # API paths 8 | class Path(object): 9 | MARKETS = 'markets' 10 | MARKET_DETAILS = 'markets/%s' 11 | TICKER = "markets/%s/ticker" 12 | ORDER_BOOK = 'markets/%s/order_book' 13 | QUOTATION = 'markets/%s/quotations' 14 | FEE_PERCENTAGE = 'markets/%s/fee_percentage' 15 | TRADE_TRANSACTIONS = 'markets/%s/trade_transactions' 16 | REPORTS = 'markets/%s/reports' 17 | BALANCES = 'balances/%s' 18 | BALANCES_EVENTS = 'balance_events' 19 | ORDERS = 'markets/%s/orders' 20 | SINGLE_ORDER = 'orders/%s' 21 | WITHDRAWALS = 'currencies/%s/withdrawals' 22 | DEPOSITS = 'currencies/%s/deposits' 23 | 24 | 25 | class Currency(_Enum): 26 | BTC = 'BTC' 27 | CLP = 'CLP' 28 | COP = 'COP' 29 | ETH = 'ETH' 30 | PEN = 'PEN' 31 | 32 | 33 | class Market(_Enum): 34 | BTC_CLP = 'BTC-CLP' 35 | BTC_COP = 'BTC-COP' 36 | BTC_PEN = 'BTC-PEN' 37 | ETH_BTC = 'ETH-BTC' 38 | ETH_CLP = 'ETH-CLP' 39 | ETH_COP = 'ETH-COP' 40 | ETH_PEN = 'ETH-PEN' 41 | 42 | @staticmethod 43 | def _format_value(value): 44 | return str(value).replace('-', '_').upper() 45 | 46 | 47 | class QuotationType(_Enum): 48 | BID_GIVEN_SIZE = 'bid_given_size' 49 | BID_GIVEN_EARNED_BASE = 'bid_given_earned_base' 50 | BID_GIVEN_SPENT_QUOTE = 'bid_given_spent_quote' 51 | ASK_GIVEN_SIZE = 'ask_given_size' 52 | ASK_GIVEN_EARNED_QUOTE = 'ask_given_earned_quote' 53 | ASK_GIVEN_SPENT_BASE = 'ask_given_spent_base' 54 | 55 | 56 | class OrderType(_Enum): 57 | ASK = 'Ask' 58 | BID = 'Bid' 59 | 60 | 61 | class OrderPriceType(_Enum): 62 | MARKET = 'market' 63 | LIMIT = 'limit' 64 | 65 | 66 | class OrderState(_Enum): 67 | RECEIVED = 'received' 68 | PENDING = 'pending' 69 | TRADED = 'traded' 70 | CANCELING = 'canceling' 71 | CANCELED = 'canceled' 72 | 73 | 74 | class BalanceEvent(_Enum): 75 | DEPOSIT_CONFIRM = 'deposit_confirm' 76 | WITHDRAWAL_CONFIRM = 'withdrawal_confirm' 77 | TRANSACTION = 'transaction' 78 | TRANSFER_CONFIRMATION = 'transfer_confirmation' 79 | 80 | 81 | class ReportType(_Enum): 82 | CANDLESTICK = 'candlestick' 83 | AVERAGE_PRICES = 'average_prices' 84 | -------------------------------------------------------------------------------- /surbtc/errors.py: -------------------------------------------------------------------------------- 1 | from . import logger 2 | 3 | 4 | class Error(Exception): 5 | pass 6 | 7 | 8 | class Unavailable(Error): 9 | pass 10 | 11 | 12 | class TradingAPIError(Error): 13 | def __init__(self, message): 14 | self.message = message 15 | logger.log_error(self.message) 16 | 17 | 18 | class InvalidResponse(TradingAPIError): 19 | def __init__(self, response): 20 | msg = ('InvalidResponse (Code: {r.status_code}): ' 21 | 'Message: {r.text} ({r.url})'. 22 | format(r=response)) 23 | super(InvalidResponse, self).__init__(msg) 24 | 25 | 26 | class DecodeError(TradingAPIError): 27 | def __init__(self): 28 | msg = 'DecodeError: Unable to decode JSON from response (no content).' 29 | super(DecodeError, self).__init__(msg) -------------------------------------------------------------------------------- /surbtc/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def log_message(msg): 5 | msg = "Trading API: " + msg 6 | return msg 7 | 8 | 9 | def log_error(msg): 10 | logging.error(log_message(msg)) 11 | return log_message(msg) 12 | 13 | 14 | def log_exception(e, msg): 15 | logging.exception(log_message(msg)) 16 | log_message(msg) 17 | 18 | 19 | def log_warning(msg): 20 | logging.warning(log_message(msg)) 21 | return log_message(msg) 22 | -------------------------------------------------------------------------------- /surbtc/models.py: -------------------------------------------------------------------------------- 1 | import math 2 | from collections import namedtuple 3 | from datetime import datetime 4 | 5 | 6 | def parse_datetime(datetime_str): 7 | if datetime_str: 8 | return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S.%fZ') 9 | 10 | 11 | def float_or_none(value): 12 | if value: 13 | return float(value) 14 | 15 | 16 | class Amount( 17 | namedtuple('amount', [ 18 | 'amount', 19 | 'currency', 20 | ]) 21 | ): 22 | 23 | @classmethod 24 | def create_from_json(cls, amount): 25 | if amount: 26 | amount = cls( 27 | amount=float(amount[0]), 28 | currency=amount[1], 29 | ) 30 | return amount 31 | 32 | 33 | class PagesMeta( 34 | namedtuple('meta', [ 35 | 'current_page', 36 | 'total_count', 37 | 'total_pages', 38 | ]) 39 | ): 40 | 41 | @classmethod 42 | def create_from_json(cls, meta): 43 | if meta: 44 | return cls( 45 | current_page=int(meta['current_page']), 46 | total_count=int(meta['total_count']), 47 | total_pages=int(meta['total_pages']), 48 | ) 49 | return meta 50 | 51 | 52 | class Market( 53 | namedtuple('market', [ 54 | 'id', 55 | 'name', 56 | 'base_currency', 57 | 'quote_currency', 58 | 'minimum_order_amount', 59 | ]), 60 | ): 61 | 62 | @classmethod 63 | def create_from_json(cls, market): 64 | return cls( 65 | id=market['id'], 66 | name=market['name'], 67 | base_currency=market['base_currency'], 68 | quote_currency=market['quote_currency'], 69 | minimum_order_amount=Amount.create_from_json( 70 | market['minimum_order_amount'] 71 | ), 72 | ) 73 | 74 | 75 | class Ticker( 76 | namedtuple('ticker', [ 77 | 'last_price', 78 | 'min_ask', 79 | 'max_bid', 80 | 'volume', 81 | 'price_variation_24h', 82 | 'price_variation_7d' 83 | ]) 84 | ): 85 | 86 | @classmethod 87 | def create_from_json(cls, ticker): 88 | return cls( 89 | last_price=Amount.create_from_json(ticker['last_price']), 90 | min_ask=Amount.create_from_json(ticker['min_ask']), 91 | max_bid=Amount.create_from_json(ticker['max_bid']), 92 | volume=Amount.create_from_json(ticker['volume']), 93 | price_variation_24h=float_or_none(ticker['price_variation_24h']), 94 | price_variation_7d=float_or_none(ticker['price_variation_7d']), 95 | ) 96 | 97 | 98 | class Quotation( 99 | namedtuple('quotation', [ 100 | 'amount', 101 | 'base_balance_change', 102 | 'base_exchanged', 103 | 'fee', 104 | 'incomplete', 105 | 'limit', 106 | 'order_amount', 107 | 'quote_balance_change', 108 | 'quote_exchanged', 109 | 'type', 110 | ]) 111 | ): 112 | 113 | @classmethod 114 | def create_from_json(cls, quotation): 115 | return cls( 116 | amount=Amount.create_from_json( 117 | quotation['amount']), 118 | base_balance_change=Amount.create_from_json( 119 | quotation['base_balance_change']), 120 | base_exchanged=Amount.create_from_json( 121 | quotation['base_exchanged']), 122 | fee=Amount.create_from_json( 123 | quotation['fee']), 124 | incomplete=quotation['incomplete'], 125 | limit=Amount.create_from_json( 126 | quotation['limit']), 127 | order_amount=Amount.create_from_json( 128 | quotation['order_amount']), 129 | quote_balance_change=Amount.create_from_json( 130 | quotation['quote_balance_change']), 131 | quote_exchanged=Amount.create_from_json( 132 | quotation['quote_exchanged']), 133 | type=quotation['type'], 134 | ) 135 | 136 | 137 | class OrderBookEntry( 138 | namedtuple('book_entry', [ 139 | 'price', 140 | 'amount', 141 | ]) 142 | ): 143 | 144 | @classmethod 145 | def create_from_json(cls, book_entry): 146 | return cls( 147 | price=float(book_entry[0]), 148 | amount=float(book_entry[1]), 149 | ) 150 | 151 | 152 | class OrderBook( 153 | namedtuple('order_book', [ 154 | 'asks', 155 | 'bids', 156 | ]) 157 | ): 158 | 159 | @classmethod 160 | def create_from_json(cls, order_book): 161 | return cls( 162 | asks=[OrderBookEntry.create_from_json(entry) 163 | for entry in order_book['asks']], 164 | bids=[OrderBookEntry.create_from_json(entry) 165 | for entry in order_book['bids']], 166 | ) 167 | 168 | 169 | class FeePercentage( 170 | namedtuple('fee_percentage', [ 171 | 'value', 172 | ]) 173 | ): 174 | 175 | @classmethod 176 | def create_from_json(cls, fee_percentage): 177 | return cls( 178 | value=float(fee_percentage['value']), 179 | ) 180 | 181 | 182 | class Balance( 183 | namedtuple('balance', [ 184 | 'id', 185 | 'account_id', 186 | 'amount', 187 | 'available_amount', 188 | 'frozen_amount', 189 | 'pending_withdraw_amount', 190 | ]) 191 | ): 192 | 193 | @classmethod 194 | def create_from_json(cls, balance): 195 | return cls( 196 | id=balance['id'], 197 | account_id=balance['account_id'], 198 | amount=Amount.create_from_json( 199 | balance['amount']), 200 | available_amount=Amount.create_from_json( 201 | balance['available_amount']), 202 | frozen_amount=Amount.create_from_json( 203 | balance['frozen_amount']), 204 | pending_withdraw_amount=Amount.create_from_json( 205 | balance['pending_withdraw_amount']), 206 | ) 207 | 208 | 209 | class Order( 210 | namedtuple('order', [ 211 | 'id', 212 | 'account_id', 213 | 'amount', 214 | 'created_at', 215 | 'fee_currency', 216 | 'limit', 217 | 'market_id', 218 | 'original_amount', 219 | 'paid_fee', 220 | 'price_type', 221 | 'state', 222 | 'total_exchanged', 223 | 'traded_amount', 224 | 'type', 225 | ]) 226 | ): 227 | 228 | @classmethod 229 | def create_from_json(cls, order): 230 | return cls( 231 | id=order['id'], 232 | account_id=order['account_id'], 233 | amount=Amount.create_from_json(order['amount']), 234 | created_at=parse_datetime(order['created_at']), 235 | fee_currency=order['fee_currency'], 236 | limit=Amount.create_from_json(order['limit']), 237 | market_id=order['market_id'], 238 | original_amount=Amount.create_from_json(order['original_amount']), 239 | paid_fee=Amount.create_from_json(order['paid_fee']), 240 | price_type=order['price_type'], 241 | state=order['state'], 242 | total_exchanged=Amount.create_from_json(order['total_exchanged']), 243 | traded_amount=Amount.create_from_json(order['traded_amount']), 244 | type=order['type'], 245 | ) 246 | 247 | 248 | class OrderPages( 249 | namedtuple('order_pages', [ 250 | 'orders', 251 | 'meta', 252 | ]) 253 | ): 254 | 255 | @classmethod 256 | def create_from_json(cls, orders, pages_meta): 257 | return cls( 258 | orders=[Order.create_from_json(order) 259 | for order in orders], 260 | meta=PagesMeta.create_from_json(pages_meta), 261 | ) 262 | 263 | 264 | class BalanceEvent( 265 | namedtuple('balance_event', [ 266 | 'id', 267 | 'account_id', 268 | 'created_at', 269 | 'currency', 270 | 'event', 271 | 'event_ids', 272 | 'new_amount', 273 | 'new_available_amount', 274 | 'new_frozen_amount', 275 | 'new_frozen_for_fee', 276 | 'new_pending_withdraw_amount', 277 | 'old_amount', 278 | 'old_available_amount', 279 | 'old_frozen_amount', 280 | 'old_frozen_for_fee', 281 | 'old_pending_withdraw_amount', 282 | 'transaction_type', 283 | 'transfer_description', 284 | ]) 285 | ): 286 | 287 | @classmethod 288 | def create_from_json(cls, event): 289 | return cls( 290 | id=event['id'], 291 | account_id=event['account_id'], 292 | created_at=parse_datetime(event['created_at']), 293 | currency=event['currency'], 294 | event=event['event'], 295 | event_ids=event['event_ids'], 296 | new_amount=event['new_amount'], 297 | new_available_amount=event['new_available_amount'], 298 | new_frozen_amount=event['new_frozen_amount'], 299 | new_frozen_for_fee=event['new_frozen_for_fee'], 300 | new_pending_withdraw_amount=event['new_pending_withdraw_amount'], 301 | old_amount=event['old_amount'], 302 | old_available_amount=event['old_available_amount'], 303 | old_frozen_amount=event['old_frozen_amount'], 304 | old_frozen_for_fee=event['old_frozen_for_fee'], 305 | old_pending_withdraw_amount=event['old_pending_withdraw_amount'], 306 | transaction_type=event['transaction_type'], 307 | transfer_description=event['transfer_description'], 308 | ) 309 | 310 | 311 | class BalanceEventPages( 312 | namedtuple('event_pages', [ 313 | 'balance_events', 314 | 'meta', 315 | ]) 316 | ): 317 | 318 | @classmethod 319 | def create_from_json(cls, events, total_count, page): 320 | return cls( 321 | balance_events=[BalanceEvent.create_from_json(event) 322 | for event in events], 323 | meta=PagesMeta( 324 | current_page=page or 1, 325 | total_count=total_count, 326 | total_pages=math.ceil(total_count / len(events))), 327 | ) 328 | 329 | 330 | class TradeTransaction( 331 | namedtuple('trade_transaction', [ 332 | 'id', 333 | 'market_id', 334 | 'created_at', 335 | 'updated_at', 336 | 'amount_sold', 337 | 'price_paid', 338 | 'ask_order', 339 | 'bid_order', 340 | 'triggering_order', 341 | ]) 342 | ): 343 | 344 | @classmethod 345 | def create_from_json(cls, transaction): 346 | return cls( 347 | id=transaction['id'], 348 | market_id=transaction['market_id'], 349 | created_at=parse_datetime(transaction['created_at']), 350 | updated_at=parse_datetime(transaction['updated_at']), 351 | amount_sold=Amount.create_from_json( 352 | [transaction['amount_sold'] / 1e8, 353 | transaction['amount_sold_currency']]), 354 | price_paid=Amount.create_from_json( 355 | [transaction['price_paid'] / 1e2, 356 | transaction['price_paid_currency']]), 357 | ask_order=Order.create_from_json(transaction['ask']), 358 | bid_order=Order.create_from_json(transaction['bid']), 359 | triggering_order=Order.create_from_json( 360 | transaction['triggering_order']), 361 | ) 362 | 363 | 364 | class TradeTransactionPages( 365 | namedtuple('trade_transaction_pages', [ 366 | 'trade_transactions', 367 | 'meta', 368 | ]) 369 | ): 370 | 371 | @classmethod 372 | def create_from_json(cls, transactions, pages_meta): 373 | return cls( 374 | trade_transactions=[TradeTransaction.create_from_json(transaction) 375 | for transaction in transactions], 376 | meta=PagesMeta.create_from_json(pages_meta), 377 | ) 378 | 379 | 380 | class TransferData( 381 | namedtuple('transfer', [ 382 | 'type', 383 | 'address', 384 | 'tx_hash', 385 | ]) 386 | ): 387 | 388 | @classmethod 389 | def create_from_json(cls, transfer, address_key): 390 | return cls( 391 | type=transfer['type'], 392 | address=transfer[address_key], 393 | tx_hash=transfer['tx_hash'] 394 | ) 395 | 396 | 397 | class Transfer( 398 | namedtuple('transfer', [ 399 | 'id', 400 | 'created_at', 401 | # 'updated_at', Missing from response? 402 | 'amount', 403 | 'fee', 404 | 'currency', 405 | 'state', 406 | 'data', 407 | ]) 408 | ): 409 | address_key = None 410 | data_key = None 411 | 412 | @classmethod 413 | def create_from_json(cls, transfer): 414 | return cls( 415 | id=transfer['id'], 416 | created_at=parse_datetime(transfer.get('created_at')), 417 | amount=Amount.create_from_json(transfer['amount']), 418 | # Fee is only returned on withdrawals 419 | fee=Amount.create_from_json(transfer.get('fee')), 420 | currency=transfer['currency'], 421 | state=transfer['state'], 422 | data=TransferData.create_from_json( 423 | transfer[cls.data_key], cls.address_key), 424 | ) 425 | 426 | 427 | class Withdrawal(Transfer): 428 | address_key = 'target_address' 429 | data_key = 'withdrawal_data' 430 | 431 | 432 | class Deposit(Transfer): 433 | address_key = 'address' 434 | data_key = 'deposit_data' 435 | 436 | 437 | class AveragePrice( 438 | namedtuple('reports', [ 439 | 'datetime', 440 | 'amount' 441 | ]) 442 | ): 443 | 444 | @classmethod 445 | def create_from_json(cls, report): 446 | 447 | return cls( 448 | datetime=report[0], 449 | amount=report[1] 450 | ) 451 | 452 | 453 | class Candlestick( 454 | namedtuple('report', [ 455 | 'datetime', 456 | 'open', 457 | 'high', 458 | 'low', 459 | 'close', 460 | 'volume' 461 | ]) 462 | ): 463 | 464 | @classmethod 465 | def create_from_json(cls, report): 466 | 467 | return cls( 468 | datetime=report[0], 469 | open=report[1], 470 | high=report[2], 471 | low=report[3], 472 | close=report[4], 473 | volume=report[5], 474 | ) 475 | -------------------------------------------------------------------------------- /surbtc/server.py: -------------------------------------------------------------------------------- 1 | from .base import Server 2 | 3 | # API Server 4 | PROTOCOL = 'https' 5 | HOST = 'www.surbtc.com/api' 6 | TEST_HOST = 'stg.surbtc.com/api' 7 | VERSION = 'v2' 8 | 9 | 10 | # SURBTC API server 11 | class SURBTCServer(Server): 12 | 13 | def __init__(self, test): 14 | host = HOST if not test else TEST_HOST 15 | Server.__init__(self, PROTOCOL, host, VERSION) 16 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delta575/python-surbtc-api/15c58aa722310316260485720af11a1c4627987a/test/__init__.py -------------------------------------------------------------------------------- /test/surbtc_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime, timedelta 3 | 4 | # pip 5 | from decouple import config 6 | 7 | # local 8 | from surbtc import SURBTC, errors, models 9 | 10 | TEST = config('SURBTC_TEST', cast=bool, default=True) 11 | API_KEY = config('SURBTC_API_KEY') 12 | API_SECRET = config('SURBTC_API_SECRET') 13 | MARKET_ID = SURBTC.Market.BTC_CLP 14 | 15 | 16 | class SURBTCPublicTest(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.client = SURBTC.Public(TEST) 20 | 21 | def test_instantiate_client(self): 22 | self.assertIsInstance(self.client, SURBTC.Public) 23 | 24 | def test_markets(self): 25 | markets = self.client.markets() 26 | self.assertEqual(len(markets), len(SURBTC.Market)) 27 | for market in markets: 28 | self.assertIsInstance(market, models.Market) 29 | 30 | def test_markets_details(self): 31 | market = self.client.market_details(MARKET_ID) 32 | self.assertIsInstance(market, models.Market) 33 | 34 | def test_ticker(self): 35 | ticker = self.client.ticker(MARKET_ID) 36 | self.assertIsInstance(ticker, models.Ticker) 37 | 38 | def test_order_book(self): 39 | order_book = self.client.order_book(MARKET_ID) 40 | self.assertIsInstance(order_book, models.OrderBook) 41 | 42 | def test_trade_transaction_pages(self): 43 | page, per_page = 2, 10 44 | trade_trans_pages = self.client.trade_transaction_pages( 45 | MARKET_ID, page=page, per_page=per_page) 46 | self.assertIsInstance(trade_trans_pages, models.TradeTransactionPages) 47 | self.assertEqual(trade_trans_pages.meta.current_page, page) 48 | self.assertEqual(len(trade_trans_pages.trade_transactions), per_page) 49 | 50 | 51 | class SURBTCAuthTest(unittest.TestCase): 52 | 53 | def setUp(self): 54 | self.client = SURBTC.Auth(API_KEY, API_SECRET, TEST) 55 | 56 | def test_instantiate_client(self): 57 | self.assertIsInstance(self.client, SURBTC.Auth) 58 | 59 | def test_quotation(self): 60 | quotation = self.client.quotation( 61 | MARKET_ID, quotation_type=SURBTC.QuotationType.ASK_GIVEN_SIZE, 62 | amount=1, limit=1) 63 | self.assertIsInstance(quotation, models.Quotation) 64 | 65 | def test_quotation_market(self): 66 | quotation = self.client.quotation( 67 | MARKET_ID, quotation_type=SURBTC.QuotationType.ASK_GIVEN_SIZE, 68 | amount=1) 69 | self.assertIsInstance(quotation, models.Quotation) 70 | 71 | def test_quotation_limit(self): 72 | quotation = self.client.quotation( 73 | MARKET_ID, quotation_type=SURBTC.QuotationType.ASK_GIVEN_SIZE, 74 | amount=1, limit=1) 75 | self.assertIsInstance(quotation, models.Quotation) 76 | 77 | def test_report_average_prices(self): 78 | end = datetime.now() 79 | start = end - timedelta(days=30) 80 | report = self.client.report_average_prices( 81 | MARKET_ID, start_at=start, end_at=end) 82 | for item in report: 83 | self.assertIsInstance(item, models.AveragePrice) 84 | 85 | def test_report_candlestick(self): 86 | end = datetime.now() 87 | start = end - timedelta(days=30) 88 | report = self.client.report_candlestick( 89 | MARKET_ID, start_at=start, end_at=end) 90 | for item in report: 91 | self.assertIsInstance(item, models.Candlestick) 92 | 93 | def test_balance(self): 94 | balance = self.client.balance(SURBTC.Currency.BTC) 95 | self.assertIsInstance(balance, models.Balance) 96 | 97 | def test_balances_event_pages(self): 98 | currencies = [item for item in SURBTC.Currency] 99 | event_names = [item for item in SURBTC.BalanceEvent] 100 | balance_events = self.client.balance_event_pages( 101 | currencies, event_names) 102 | self.assertIsInstance(balance_events, models.BalanceEventPages) 103 | 104 | def test_withdrawals(self): 105 | withdrawals = self.client.withdrawals(currency=SURBTC.Currency.BTC) 106 | for withdrawal in withdrawals: 107 | self.assertIsInstance(withdrawal, models.Withdrawal) 108 | 109 | def test_deposits(self): 110 | deposits = self.client.deposits(currency=SURBTC.Currency.BTC) 111 | for deposit in deposits: 112 | self.assertIsInstance(deposit, models.Deposit) 113 | 114 | def test_simulate_withdrawal(self): 115 | simulate_withdrawal = self.client.simulate_withdrawal( 116 | currency=SURBTC.Currency.BTC, amount=0) 117 | self.assertIsInstance(simulate_withdrawal, models.Withdrawal) 118 | 119 | def test_order_pages(self): 120 | page, per_page = 2, 10 121 | order_pages = self.client.order_pages( 122 | MARKET_ID, page=page, per_page=per_page) 123 | self.assertIsInstance(order_pages, models.OrderPages) 124 | self.assertEqual(order_pages.meta.current_page, page) 125 | self.assertEqual(len(order_pages.orders), per_page) 126 | 127 | def test_order_details(self): 128 | orders = self.client.order_pages( 129 | MARKET_ID, page=1, per_page=1).orders 130 | first_order = orders[0] 131 | single_order = self.client.order_details(first_order.id) 132 | self.assertIsInstance(single_order, models.Order) 133 | 134 | @unittest.skipUnless(TEST, 'Only run on staging context') 135 | def test_new_order_cancel_order(self): 136 | # New order 137 | new_order = self.client.new_order( 138 | MARKET_ID, SURBTC.OrderType.ASK, SURBTC.OrderPriceType.LIMIT, 139 | amount=0.001, limit=100000) 140 | # Cancel order 141 | canceled_order = self.client.cancel_order(new_order.id) 142 | # Assertions 143 | self.assertIsInstance(new_order, models.Order) 144 | self.assertIsInstance(canceled_order, models.Order) 145 | 146 | 147 | class SURBTCAuthTestBadApi(unittest.TestCase): 148 | 149 | def setUp(self): 150 | self.client = SURBTC.Auth('BAD_KEY', 'BAD_SECRET') 151 | 152 | def test_instantiate_client(self): 153 | self.assertIsInstance(self.client, SURBTC.Auth) 154 | 155 | def test_key_secret(self): 156 | self.assertRaises(ValueError, 157 | lambda: SURBTC.Auth()) 158 | 159 | def test_balance_returns_error(self): 160 | self.assertRaises(errors.InvalidResponse, 161 | lambda: self.client.balance(SURBTC.Currency.CLP)) 162 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py35, py36 4 | 5 | [testenv] 6 | setenv = VIRTUAL_ENV={envdir} 7 | deps = -r{toxinidir}/requirements.txt 8 | commands= 9 | py.test \ 10 | {posargs} --------------------------------------------------------------------------------