├── .gitignore ├── CHANGES.md ├── MANIFEST.in ├── README.md ├── VERSION ├── binance ├── __init__.py ├── __main__.py ├── cache.py ├── client.py ├── enums.py ├── storage.py └── utils.py ├── config.yaml ├── pytest.ini ├── scripts ├── __init__.py ├── watch_candlesticks.py └── watch_depth.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_actions.py └── test_fetches.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | notes.txt 106 | development.yaml 107 | production.yaml 108 | test_config.yaml 109 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0lon/binance-api-python/419bbadbb8e19a563c6e012a60961db7668a5731/CHANGES.md -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md CHANGES.md VERSION 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # binance 2 | 3 | Python client for the 4 | [Binance API](https://www.binance.com/restapipub.html). 5 | 6 | Requires python 3.6 or greater. 7 | 8 | ## Installation 9 | 10 | ``` 11 | git clone https://github.com/c0lon/binance.git 12 | cd binance 13 | python setup.py install 14 | ``` 15 | 16 | ## Tests 17 | 18 | First, enter your API key and secret into 19 | [config.yaml](config.yaml). 20 | Then run the command below: 21 | 22 | `python setup.py test` 23 | 24 | Any log messages are written to `tests/test.log`. 25 | 26 | To enter a `pdb` shell on a test failure, run 27 | 28 | `pytest --pdb` 29 | 30 | See the 31 | [pytest docs](https://docs.pytest.org/en/latest/contents.html) 32 | for more information. 33 | 34 | ### Enabling all tests 35 | 36 | By default, tests that would change your account in any way, 37 | such as placing an order or withdrawing funds, are disabled. 38 | To enable them, edit [pytest.ini](pytest.ini) by changing 39 | 40 | ``` 41 | [pytest] 42 | testpaths = tests/test_fetchers.py 43 | ``` 44 | 45 | to 46 | 47 | ``` 48 | [pytest] 49 | testpaths = tests 50 | ``` 51 | 52 | then follow the instructions in 53 | [tests/test_actions.py](tests/test_actions.py). 54 | 55 | #### WARNING 56 | 57 | Enabling these tests means that your account balances will be 58 | changed if the tests are successful. Follow the configuration 59 | instructions **very carefully.** Failing to do so could result 60 | in the tests altering your account in a negative way. 61 | 62 | ## Usage 63 | 64 | ```python 65 | from binance import BinanceClient 66 | 67 | client = BinanceClient(apikey, apisecret) 68 | client.ping() 69 | ``` 70 | 71 | 72 | ### Storage Classes 73 | 74 | Most client methods described below return objects that can be found 75 | in [binance/storage.py](binance/storage.py). These objects convert 76 | values into their natives types, including: 77 | * decimal strings to `float` 78 | * timestamps to `datetime.datetime` 79 | 80 | Each object accepts a raw API version of the object in its constructor. 81 | These objects also have a `to_json()` method that returns the object 82 | as a JSON-dumpable dictionary. 83 | 84 | 85 | ### Client Methods 86 | 87 | Methods with names ending with `_async` are asynchronous `coroutines` 88 | that perform the same action as their synchronous counterpart. 89 | (Read more about Python's asynchronous features 90 | [here](https://docs.python.org/3/library/asyncio.html).) 91 | 92 | #### Public Endpoint Methods 93 | 94 | ##### `/ping` 95 | 96 | ``` 97 | def ping() 98 | ``` 99 | 100 | ##### `/time` 101 | Return the server time in milliseconds as an integer. 102 | ``` 103 | def get_server_time() 104 | ``` 105 | 106 | ##### `/ticker` 107 | Return `binance.storage.Ticker`. 108 | ``` 109 | def get_ticker(self, symbol='') 110 | ``` 111 | 112 | ##### `/depth` 113 | Return `binance.storage.Depth`. 114 | ``` 115 | def get_depth(self, symbol) 116 | async def get_depth_async(self, symbol) 117 | ``` 118 | 119 | ##### `/klines` 120 | Return list of `binance.storage.Candlestick`. 121 | ``` 122 | def get_candlesticks(self, symbol, interval, **kwargs) 123 | async def get_candlesticks_async(self, symbol, interval, **kwargs) 124 | ``` 125 | 126 | #### Signed Endpoint Methods 127 | 128 | ##### `/myTrades` 129 | Return list of `binance.storage.Trade`. 130 | ``` 131 | def get_trade_info(self, symbol) 132 | ``` 133 | 134 | ##### `/openOrders` 135 | Return list of `binance.storage.Order`. 136 | ``` 137 | def get_open_orders(self, symbol) 138 | ``` 139 | 140 | ##### `/allOrders` 141 | Return list of `binance.storage.Order`. 142 | ``` 143 | def get_all_orders(self, symbol): 144 | ``` 145 | 146 | ##### `/order` 147 | Return `binance.storage.Order` 148 | ``` 149 | def get_order_status(self, symbol, order_id) 150 | def place_market_buy(self, symbol, quantity, **kwargs) 151 | def place_market_sell(self, symbol, quantity, **kwargs) 152 | def place_limit_buy(self, symbol, quantity, price, **kwargs) 153 | def place_limit_sell(self, symbol, quantity, price, **kwargs) 154 | ``` 155 | Return `True` if order was canceled successfully. 156 | ``` 157 | def cancle_order(self, order_id) 158 | ``` 159 | 160 | ##### `/withdraw` 161 | Return `True` if the withdraw is successfully initiated. 162 | ``` 163 | def withdraw(self, asset, amount, address, **kwargs) 164 | ``` 165 | 166 | ##### `/withdrawHistory.html` 167 | Return list of `binance.storage.Withdraw`. 168 | ``` 169 | def get_withdraw_history(self, asset='', **kwargs) 170 | ``` 171 | 172 | ##### `/depositHistory.html` 173 | Return list of `binance.storage.Deposit`. 174 | ``` 175 | def get_deposit_history(self, asset='', **kwargs) 176 | ``` 177 | 178 | #### Websocket Endpoint Methods 179 | 180 | ##### `@depth` 181 | ``` 182 | def watch_depth(self, symbol) 183 | ``` 184 | See [watch_depth.py](scripts/watch_depth.py) for an example of how to 185 | use the asynchronous `watch_depth()` method. 186 | 187 | ##### `@kline` 188 | ``` 189 | def watch_candlesticks(self, symbol) 190 | ``` 191 | See [watch_candlesticks.py](scripts/watch_candlesticks.py) for an 192 | example of how to use the asynchronous `watch_candlesticks()` method. 193 | 194 | #### Event Callback Methods 195 | ``` 196 | def event(self, coro) 197 | ``` 198 | Register an asynchronous `coroutine` that is fired on certain client 199 | events. 200 | 201 | Supported Events: 202 | * `on_depth_ready` 203 | * `on_depth_event` 204 | * `on_candlesticks_ready` 205 | * `on_candlesticks_event` 206 | 207 | See [scripts/watch_depth.py](scripts/watch_depth.py) and 208 | [scripts/watch_candlesticks.py](scripts/watch_candlesticks.py) 209 | for examples. 210 | 211 | 212 | ### Scripts 213 | 214 | In order for the scripts below to work correctly, you must put your 215 | `apiKey` and `secretKey` into the `apikey` and `apisecret` slots 216 | in [config.yaml](config.yaml), respectively. 217 | 218 | #### [watchdepth](scripts/watch_depth.py) 219 | ``` 220 | usage: watchdepth [-h] [--log-level {DEBUG,INFO,WARN,ERROR,CRITICAL}] 221 | [--version] [--debug] [-l DEPTH_LIMIT] 222 | config_uri symbol 223 | 224 | positional arguments: 225 | config_uri the config file to use. 226 | symbol watch the depth of symbol . 227 | 228 | optional arguments: 229 | -h, --help show this help message and exit 230 | --log-level {DEBUG,INFO,WARN,ERROR,CRITICAL} 231 | --version Show the package version and exit. 232 | --debug 233 | -l DEPTH_LIMIT, --depth-limit DEPTH_LIMIT 234 | show the latest orders on each side. 235 | ``` 236 | 237 | #### [watchcandlesticks](scripts/watch_candlesticks.py) 238 | ``` 239 | usage: watchcandlesticks [-h] [--log-level {DEBUG,INFO,WARN,ERROR,CRITICAL}] 240 | [--version] [--debug] [-d DEPTH] 241 | config_uri symbol interval 242 | 243 | positional arguments: 244 | config_uri the config file to use. 245 | symbol watch the candlesticks of symbol . 246 | interval set the candlesticks interval. 247 | 248 | optional arguments: 249 | -h, --help show this help message and exit 250 | --log-level {DEBUG,INFO,WARN,ERROR,CRITICAL} 251 | --version Show the package version and exit. 252 | --debug 253 | -d DEPTH, --depth DEPTH 254 | display the latest candlesticks. 255 | ``` 256 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 2 | -------------------------------------------------------------------------------- /binance/__init__.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import logging.config 3 | import os 4 | import sys 5 | 6 | import yaml 7 | 8 | from .client import BinanceClient 9 | 10 | 11 | here = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 12 | with open(os.path.join(here, 'VERSION')) as f: 13 | __version__ = f.read().strip() 14 | __author__ = 'c0lon' 15 | __email__ = '' 16 | 17 | 18 | def get_default_arg_parser(): 19 | arg_parser = ArgumentParser() 20 | arg_parser.add_argument('config_uri', type=str, 21 | help='the config file to use.') 22 | arg_parser.add_argument('--log-level', type=str, 23 | choices=['DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL']) 24 | arg_parser.add_argument('--version', action='store_true', 25 | help='Show the package version and exit.') 26 | arg_parser.add_argument('--debug', action='store_true') 27 | 28 | return arg_parser 29 | 30 | 31 | def configure_app(config_uri='', arg_parser=None): 32 | ''' Configure the application. 33 | 34 | :param config_uri: 35 | If != '', app is configured using the file at the given 36 | path, if it exists. Else require a config_uri as a command 37 | line argument. 38 | :type config_uri: str 39 | :param arg_parser: 40 | If None, a minimal argument parser is created. A config uri 41 | is required and the log level can be optionally set. 42 | :type arg_parser: argparse.ArgumentParser 43 | 44 | :return: 45 | Return a tuple of the `main` section of the config and 46 | the entire config object. 47 | :rtype: ({}, {}) 48 | ''' 49 | 50 | args = {'config_uri' : config_uri} 51 | if not args['config_uri']: 52 | arg_parser = arg_parser or get_default_arg_parser() 53 | args = vars(arg_parser.parse_args()) 54 | 55 | if args.pop('version', None): 56 | print(__version__) 57 | sys.exit(0) 58 | 59 | try: 60 | with open(args['config_uri']) as f: 61 | config = yaml.load(f) 62 | except: 63 | print('invalid config file: {}'.format(args['config_uri'])) 64 | sys.exit(1) 65 | else: 66 | config['args'] = args 67 | 68 | if args.get('log_level'): 69 | config['logging']['root']['level'] = args.pop('log_level') 70 | logging.config.dictConfig(config['logging']) 71 | 72 | config['main'] = config.get('main', {}) 73 | config['main']['debug'] = args.get('debug', False) 74 | 75 | return config['main'], config 76 | -------------------------------------------------------------------------------- /binance/__main__.py: -------------------------------------------------------------------------------- 1 | from . import configure_app 2 | 3 | 4 | def main(): 5 | settings, global_config = configure_app() 6 | 7 | 8 | if __name__ == '__main__': 9 | main() 10 | -------------------------------------------------------------------------------- /binance/cache.py: -------------------------------------------------------------------------------- 1 | """ DepthCache helper class for the Binance API Client. 2 | """ 3 | 4 | 5 | from collections import deque 6 | 7 | from .storage import ( 8 | Bid, 9 | Ask, 10 | ) 11 | from .utils import GetLoggerMixin 12 | 13 | 14 | class DepthCache(GetLoggerMixin): 15 | __loggername__ = 'DepthCache' 16 | 17 | def __init__(self): 18 | self.bids = [] 19 | self.asks = [] 20 | 21 | self.received_api_response = False 22 | self.event_queue = deque() 23 | self.last_update_id = -1 24 | 25 | def _update(self, event): 26 | logger = self._logger('_update') 27 | 28 | if event['u'] < self.last_update_id: return 29 | logger.debug(event['u']) 30 | 31 | self.last_update_id = event['u'] 32 | event_bids = {b[0]: Bid(b) for b in event['bids']} 33 | event_asks = {a[0]: Ask(a) for a in event['asks']} 34 | 35 | updated_bids = [] 36 | for bid in self.bids: 37 | event_bid = event_bids.get(bid.price) 38 | if not event_bid: 39 | updated_asks.append(ask) 40 | elif not event_bid.quantity: 41 | continue 42 | else: 43 | updates_bids.append(event_bid) 44 | self.bids = updated_bids 45 | 46 | updated_asks = [] 47 | for ask in self.asks: 48 | event_ask = event_asks.get(ask.price) 49 | if not event_ask: 50 | updated_asks.append(ask) 51 | elif not event_ask.quantity: 52 | continue 53 | else: 54 | updated_asks.append(ask) 55 | self.asks = updated_asks 56 | 57 | def set_initial_data(self, depth): 58 | logger = self._logger('set_initial_data') 59 | 60 | self.last_update_id = depth.update_id 61 | logger.debug(f'set_initial_data: {self.last_update_id}') 62 | 63 | self.bids = depth.bids 64 | self.asks = depth.asks 65 | while self.event_queue: 66 | event = self.event_queue.popleft() 67 | self._update(event) 68 | 69 | self.received_api_response = True 70 | 71 | def pretty_print(self, depth=40): 72 | if depth: 73 | bids = self.bids[:depth] 74 | asks = self.asks[:depth] 75 | else: 76 | bids = self.bids 77 | asks = self.asks 78 | 79 | print('Bids') 80 | for bid in bids: 81 | print(f'{bid.price:12f} : {bid.quantity:12f}') 82 | 83 | print('\nAsks') 84 | for ask in asks: 85 | print(f'{ask.price:12f} : {ask.quantity:12f}') 86 | print() 87 | 88 | 89 | class CandlestickCache(GetLoggerMixin): 90 | __loggername__ = 'CandlestickCache' 91 | 92 | def __init__(self): 93 | self.candlesticks = [] 94 | self.received_api_response = False 95 | self.depth = 0 96 | 97 | def update(self, event): 98 | if self.received_api_response: 99 | self._update(event) 100 | 101 | def _update(self, event): 102 | logger = self._logger('_update') 103 | 104 | event_candlestick = Candlestick.from_websocket_event(event) 105 | latest_candlestick = self.candlesticks[-1] 106 | 107 | # if the event candlestick has the same time window 108 | # as the latest candlestick, update the latest candlestick 109 | # 110 | # if the event candlestick is of a newer time window 111 | # than the latest candlestick, add it to the list 112 | # keep candlestick list a certain length 113 | if event_candlestick.open_time == latest_candlestick.open_time: 114 | self.candlesticks[-1] = event_candlestick 115 | elif event_candlestick.open_time > latest_candlestick.open_time: 116 | self.candlesticks.append(event_candlestick) 117 | if len(self.candlesticks) > self.depth: 118 | self.candlesticks.pop(0) 119 | 120 | def set_initial_data(self, candlesticks): 121 | self._logger().info('set_initial_data') 122 | 123 | self.candlesticks = candlesticks 124 | self.depth = len(self.candlesticks) 125 | self.received_api_response = True 126 | 127 | def pretty_print(self, depth=40): 128 | if depth: 129 | candlesticks = self.candlesticks[-depth:] 130 | else: 131 | candlesticks = self.candlesticks 132 | 133 | for candlestick in candlesticks: 134 | date_string = candlestick.open_time.strftime('%Y-%m-%d %H:%M:%S') 135 | print(f'{candlestick.symbol} {date_string}') 136 | print(f' open: {candlestick.price.open}') 137 | print(f' high: {candlestick.price.high}') 138 | print(f' low: {candlestick.price.low}') 139 | print(f' close: {candlestick.price.low}') 140 | print(f' volume: {candlestick.volume}') 141 | print() 142 | -------------------------------------------------------------------------------- /binance/client.py: -------------------------------------------------------------------------------- 1 | """ Binance API Client. 2 | """ 3 | 4 | 5 | import asyncio 6 | import hashlib 7 | import hmac 8 | import json 9 | import time 10 | from urllib.parse import quote 11 | 12 | import aiohttp 13 | import requests 14 | import websockets as ws 15 | 16 | from .cache import ( 17 | DepthCache, 18 | CandlestickCache, 19 | ) 20 | from .enums import ( 21 | OrderSides, 22 | OrderTypes, 23 | TimeInForce, 24 | ) 25 | from .storage import ( 26 | Account, 27 | Candlestick, 28 | Deposit, 29 | Depth, 30 | Order, 31 | Ticker, 32 | Trade, 33 | Withdraw, 34 | ) 35 | from .utils import GetLoggerMixin 36 | 37 | 38 | API_BASE_URL = 'https://www.binance.com' 39 | 40 | WEBSOCKET_BASE_URL = 'wss://stream.binance.com:9443/ws/{symbol}' 41 | DEPTH_WEBSOCKET_URL = '{}@depth'.format(WEBSOCKET_BASE_URL) 42 | KLINE_WEBSOCKET_URL = '{}@kline'.format(WEBSOCKET_BASE_URL) 43 | 44 | CONTENT_TYPE = 'x-www-form-urlencoded' 45 | 46 | 47 | class Endpoints: 48 | PING = 'api/v1/ping' 49 | SERVER_TIME = 'api/v1/time' 50 | ACCOUNT_INFO = 'api/v3/account' 51 | TRADE_INFO = 'api/v3/myTrades' 52 | ORDER = 'api/v3/order' 53 | ALL_ORDERS = 'api/v3/allOrders' 54 | OPEN_ORDERS = 'api/v3/openOrders' 55 | TICKER_ALL = 'api/v1/ticker/allPrices' 56 | TICKER_BEST = '/api/v1/ticker/allBookTickers' 57 | TICKER_24HR = '/api/v1/ticker/ticker/24hr' 58 | DEPTH = 'api/v1/depth' 59 | KLINES = 'api/v1/klines' 60 | WITHDRAW = 'wapi/v1/withdraw.html' 61 | WITHDRAW_HISTORY = 'wapi/v1/getWithdrawHistory.html' 62 | DEPOSIT_HISTORY = 'wapi/v1/getDepositHistory.html' 63 | 64 | 65 | class BinanceClient(GetLoggerMixin): 66 | __loggername__ = 'BinanceClient' 67 | 68 | def __init__(self, apikey, apisecret): 69 | if not apikey or not apisecret: 70 | self._logger().error('invalid api key/secret') 71 | raise ValueError('invalid api key/secret') 72 | 73 | self.apikey = apikey 74 | self.apisecret = apisecret 75 | self.headers = { 76 | 'X-MBX-APIKEY' : self.apikey, 77 | 'content_type' : CONTENT_TYPE 78 | } 79 | 80 | self._loop = asyncio.get_event_loop() 81 | self.depth_cache = {} 82 | self.candlestick_cache = {} 83 | 84 | def _prepare_request(self, path, verb, params, signed): 85 | params = params or {} 86 | 87 | if signed: 88 | url = self._sign_request(path, params) 89 | elif params: 90 | query_string = self._get_sorted_query_string(params) 91 | url = '{}/{}?{}'.format(API_BASE_URL, path, query_string) 92 | else: 93 | url = '{}/{}'.format(API_BASE_URL, path) 94 | 95 | return url 96 | 97 | def _make_request(self, path, verb='get', params=None, signed=False): 98 | logger = self._logger('_make_request') 99 | 100 | verb = verb.lower() 101 | url = self._prepare_request(path, verb, params, signed) 102 | logger.info(f'{verb.upper()} {url}') 103 | 104 | http_function = getattr(requests, verb) 105 | response = http_function(url, headers=self.headers) 106 | response_json = response.json() 107 | 108 | # don't overwrite 'msg' in log record 109 | if 'msg' in response_json: 110 | response_json['message'] = response_json.pop('msg') 111 | 112 | if response.ok: 113 | return response.json() 114 | 115 | logger.error(f'error: {response.reason}', exc_info=True) 116 | logger.debug(response_json['message'], extra=response_json) 117 | 118 | raise response.raise_for_status() 119 | 120 | async def _make_request_async(self, path, verb='get', params=None, signed=False): 121 | logger = self._logger('_make_request_async') 122 | 123 | verb = verb.lower() 124 | url = self._prepare_request(path, verb, params, signed) 125 | logger.info(f'{verb.upper()} {url}') 126 | 127 | async with aiohttp.ClientSession() as client: 128 | http_function = getattr(client, verb) 129 | response = await http_function(url, headers=self.headers) 130 | response_json = await response.json(content_type=None) 131 | 132 | # don't overwrite 'msg' in log record 133 | if 'msg' in response_json: 134 | response_json['message'] = response_json.pop('msg') 135 | 136 | if response.reason == 'OK': 137 | logger.debug('success', extra={'response' : response_json}) 138 | return response_json 139 | 140 | logger.error(f'error: {response.reason}', exc_info=True) 141 | logger.debug(response_json['message'], extra=response_json) 142 | 143 | response.raise_for_status() 144 | 145 | def _get_sorted_query_string(self, params): 146 | sorted_parameters = [] 147 | for param in sorted(params.keys()): 148 | url_encoded_value = quote(str(params[param])) 149 | sorted_parameters.append('{}={}'.format(param, url_encoded_value)) 150 | 151 | return '&'.join(sorted_parameters) 152 | 153 | def _sign_request(self, path, params): 154 | url = '{}/{}'.format(API_BASE_URL, path) 155 | 156 | params['timestamp'] = int(round(time.time() * 1000.0)) 157 | if 'recvWindow' not in params: params['recvWindow'] = 6000 158 | query_string = self._get_sorted_query_string(params) 159 | 160 | signature = hmac.new( 161 | self.apisecret.encode(), 162 | digestmod=hashlib.sha256) 163 | signature.update(query_string.encode()) 164 | 165 | return '{}?{}&signature={}'.format( 166 | url, query_string, signature.hexdigest()) 167 | 168 | def ping(self): 169 | self._make_request(Endpoints.PING) 170 | return True 171 | 172 | def get_server_time(self): 173 | server_time = self._make_request(Endpoints.SERVER_TIME) 174 | return server_time['serverTime'] 175 | 176 | def get_ticker(self, symbol=''): 177 | self._logger('get_ticker').info(symbol) 178 | raw_tickers = self._make_request(Endpoints.TICKER_ALL) 179 | 180 | if symbol: 181 | for raw_ticker in raw_tickers: 182 | if raw_ticker['symbol'] == symbol: 183 | return Ticker(raw_ticker) 184 | else: 185 | raise ValueError(f'invalid symbol: {symbol}') 186 | else: 187 | return [Ticker(rt) for rt in raw_tickers] 188 | 189 | def get_depth(self, symbol): 190 | self._logger('get_depth').info(symbol) 191 | depth = self._make_request(Endpoints.DEPTH, params={'symbol' : symbol}) 192 | 193 | return Depth(symbol, depth) 194 | 195 | async def get_depth_async(self, symbol, **kwargs): 196 | self._logger('get_depth_async').info(symbol) 197 | raw_depth = await self._make_request_async(Endpoints.DEPTH, 198 | params={'symbol': symbol}) 199 | 200 | depth = Depth(symbol, raw_depth) 201 | await self._handle_callback(kwargs.get('callback'), depth) 202 | 203 | return depth 204 | 205 | def watch_depth(self, symbol): 206 | self._logger('watch_depth').info(symbol) 207 | 208 | cache = self.depth_cache.get(symbol) 209 | if not cache: 210 | cache = DepthCache() 211 | self.depth_cache[symbol] = cache 212 | 213 | async def _watch_for_depth_events(): 214 | logger = self._logger('_watch_for_depth_events') 215 | 216 | url = DEPTH_WEBSOCKET_URL.format(symbol=symbol.lower()) 217 | logger.debug(f'opening websocket connection: {url}') 218 | async with ws.connect(url) as socket: 219 | while True: 220 | event = await socket.recv() 221 | try: 222 | event_dict = json.loads(event) 223 | logger.debug(f'event: {event_dict["u"]}') 224 | cache.update(event_dict) 225 | except: 226 | pass 227 | 228 | if hasattr(self, 'on_depth_event'): 229 | logger.debug('on_depth_event') 230 | await self.on_depth_event(event_dict) 231 | 232 | async def _get_initial_depth_info(): 233 | logger = self._logger('_get_initial_depth_info') 234 | 235 | depth = await self.get_depth_async(symbol) 236 | cache.set_initial_data(depth) 237 | logger.debug('depth ready') 238 | 239 | if hasattr(self, 'on_depth_ready'): 240 | logger.debug('on_depth_ready') 241 | await self.on_depth_ready(depth) 242 | 243 | self._loop.run_until_complete(asyncio.gather( 244 | _watch_for_depth_events(), 245 | _get_initial_depth_info() 246 | )) 247 | 248 | def get_candlesticks(self, symbol, interval, **kwargs): 249 | self._logger('get_candlesticks').info(f'{symbol} {interval}') 250 | 251 | params = { 252 | 'symbol' : symbol, 253 | 'interval' : interval, 254 | 'limit' : kwargs.get('limit', 500) 255 | } 256 | if 'start_time' in kwargs: 257 | params['startTime'] = kwargs['start_time'] 258 | if 'end_time' in kwargs: 259 | params['endTime'] = kwargs['end_time'] 260 | 261 | raw_candlesticks = self._make_request(Endpoints.KLINES, 262 | verb='get', params=params) 263 | return [Candlestick(symbol, cs) for cs in raw_candlesticks] 264 | 265 | async def get_candlesticks_async(self, symbol, interval, **kwargs): 266 | logger = self._logger('get_candlesticks_async') 267 | logger.info(f'{symbol} {interval}') 268 | 269 | params = { 270 | 'symbol' : symbol, 271 | 'interval' : interval, 272 | 'limit' : kwargs.get('limit', 500) 273 | } 274 | if 'start_time' in kwargs: 275 | params['startTime'] = kwargs['start_time'] 276 | if 'end_time' in kwargs: 277 | params['endTime'] = kwargs['end_time'] 278 | 279 | raw_candlesticks = await self._make_request_async(Endpoints.KLINES, 280 | verb='get', params=params) 281 | candlesticks = [Candlestick(symbol, cs) for cs in raw_candlesticks] 282 | await self._handle_callback(kwargs.get('callback'), candlesticks) 283 | 284 | return candlesticks 285 | 286 | async def _handle_callback(self, callback, *values): 287 | if not callback: 288 | return 289 | 290 | if asyncio.iscoroutinefunction(callback): 291 | await callback(*values) 292 | elif hasattr(callback, '__call__'): 293 | callback(*values) 294 | else: 295 | logger.error(f'callback function {callback.__name__} must be a function or a coroutine, not "{type(callback).__name__}"') 296 | 297 | def watch_candlesticks(self, symbol, interval, **kwargs): 298 | self._logger('watch_candlesticks').info(f'{symbol} {interval}') 299 | 300 | cache = self.candlestick_cache.get((symbol, interval)) 301 | if not cache: 302 | cache = CandlestickCache() 303 | self.candlestick_cache[(symbol, interval)] = cache 304 | 305 | async def _watch_for_candlesticks_events(): 306 | logger = self._logger('_watch_for_candlestick_events') 307 | 308 | url = KLINE_WEBSOCKET_URL.format(symbol=symbol.lower()) 309 | url += '_{}'.format(interval) 310 | logger.debug(f'opening websocket connection: {url}') 311 | async with ws.connect(url) as socket: 312 | while True: 313 | event = await socket.recv() 314 | try: 315 | event_dict = json.loads(event) 316 | logger.debug(f'event: {event_dict["E"]}') 317 | cache.update(event_dict) 318 | except: 319 | pass 320 | 321 | if hasattr(self, 'on_candlesticks_event'): 322 | logger.debug('on_candlesticks_event') 323 | await self.on_candlesticks_event(event_dict) 324 | 325 | async def _get_initial_candlesticks_info(): 326 | logger = self._logger('_get_initial_candlesticks_info') 327 | 328 | candlesticks = await self.get_candlesticks_async(symbol, interval) 329 | cache.set_initial_data(candlesticks) 330 | logger.debug('candlesticks ready') 331 | 332 | if hasattr(self, 'on_candlesticks_ready'): 333 | logger.debug('on_candlesticks_ready') 334 | await self.on_candlesticks_ready() 335 | 336 | self._loop.run_until_complete(asyncio.gather( 337 | _watch_for_candlesticks_events(), 338 | _get_initial_candlesticks_info() 339 | )) 340 | 341 | def get_account_info(self): 342 | self._logger().info('get_account_info') 343 | raw_account = self._make_request(Endpoints.ACCOUNT_INFO, signed=True) 344 | return Account(raw_account) 345 | 346 | def get_trade_info(self, symbol): 347 | self._logger('get_trade_info').info(symbol) 348 | raw_trades = self._make_request(Endpoints.TRADE_INFO, 349 | signed=True, params={'symbol' : symbol}) 350 | 351 | return [Trade(symbol, t) for t in raw_trades] 352 | 353 | def get_open_orders(self, symbol): 354 | self._logger('get_open_orders').info(symbol) 355 | raw_orders = self._make_request(Endpoints.OPEN_ORDERS, 356 | signed=True, params={'symbol' : symbol}) 357 | 358 | return [Order(o) for o in raw_orders] 359 | 360 | def get_all_orders(self, symbol): 361 | self._logger('get_all_orders').info(symbol) 362 | raw_orders = self._make_request(Endpoints.ALL_ORDERS, 363 | signed=True, params={'symbol' : symbol}) 364 | 365 | return [Order(o) for o in raw_orders] 366 | 367 | def get_order_status(self, symbol, order_id): 368 | self._logger('get_order_status').info(f'{symbol}: {order_id}') 369 | raw_order = self._make_request(Endpoints.ORDER, signed=True, 370 | params={'symbol' : symbol, 'orderId' : order_id}) 371 | 372 | return Order(raw_order) 373 | 374 | def cancel_order(self, symbol, order_id): 375 | self._logger('cancel_order').info(f'{symbol}: {order_id}') 376 | raw_order = self._make_request(Endpoints.ORDER, verb='delete', signed=True, 377 | params={'symbol' : symbol, 'orderId' : order_id}) 378 | 379 | return True 380 | 381 | def place_market_buy(self, symbol, quantity, **kwargs): 382 | self._logger('place_market_buy').info(f'{symbol}: {quantity}') 383 | 384 | params = { 385 | 'symbol' : symbol, 386 | 'side' : OrderSides.BUY, 387 | 'type' : OrderTypes.MARKET, 388 | 'quantity' : quantity, 389 | 'recvWindow' : 60000 390 | } 391 | raw_order = self._make_request(Endpoints.ORDER, 392 | verb='post', signed=True, params=params) 393 | 394 | return Order(raw_order) 395 | 396 | def place_market_sell(self, symbol, quantity, **kwargs): 397 | self._logger('place_market_sell').info(f'{symbol}: {quantity}') 398 | 399 | params = { 400 | 'symbol' : symbol, 401 | 'side' : OrderSides.SELL, 402 | 'type' : OrderTypes.MARKET, 403 | 'quantity' : quantity, 404 | 'recvWindow' : 60000 405 | } 406 | raw_order = self._make_request(Endpoints.ORDER, 407 | verb='post', signed=True, params=params) 408 | 409 | return Order(raw_order) 410 | 411 | def place_limit_buy(self, symbol, quantity, price, **kwargs): 412 | self._logger('place_limit_buy').info(f'{symbol}: {quantity} @ {price}') 413 | 414 | params = { 415 | 'symbol' : symbol, 416 | 'side' : OrderSides.BUY, 417 | 'type' : OrderTypes.LIMIT, 418 | 'timeInForce' : kwargs.get('time_in_force', TimeInForce.GTC), 419 | 'quantity' : quantity, 420 | 'price' : price, 421 | 'recvWindow' : 60000 422 | } 423 | if 'stop_price' in kwargs: 424 | params['stopPrice'] = kwargs['stop_price'] 425 | 426 | raw_order = self._make_request(Endpoints.ORDER, 427 | verb='post', signed=True, params=params) 428 | 429 | return Order(raw_order) 430 | 431 | def place_limit_sell(self, symbol, quantity, price, **kwargs): 432 | self._logger('place_limit_sell').info(f'{symbol}: {quantity} @ {price}') 433 | 434 | params = { 435 | 'symbol' : symbol, 436 | 'side' : OrderSides.SELL, 437 | 'type' : OrderTypes.LIMIT, 438 | 'timeInForce' : kwargs.get('time_in_force', TimeInForce.GTC), 439 | 'quantity' : quantity, 440 | 'price' : price, 441 | 'recvWindow' : 60000 442 | } 443 | if 'stop_price' in kwargs: 444 | params['stopPrice'] = kwargs['stop_price'] 445 | 446 | raw_order = self._make_request(Endpoints.ORDER, 447 | verb='post', signed=True, params=params) 448 | 449 | return Order(raw_order) 450 | 451 | def withdraw(self, asset, amount, address, **kwargs): 452 | logger = self._logger('withdraw') 453 | logger.info(f'{amount} {asset} -> {address}') 454 | 455 | params = { 456 | 'asset' : asset, 457 | 'amount' : amount, 458 | 'address' : address 459 | } 460 | response = self._make_request(Endpoints.WITHDRAW, 461 | verb='post', signed=True, params=params) 462 | if not response.get('success'): 463 | logger.error('failed request', extra=response) 464 | return 465 | 466 | return response['success'] 467 | 468 | def get_withdraw_history(self, asset='', **kwargs): 469 | logger = self._logger('get_withdraw_history') 470 | 471 | params = {} 472 | if asset: 473 | logger.info(asset) 474 | params['asset'] = asset 475 | 476 | response = self._make_request(Endpoints.WITHDRAW_HISTORY, 477 | verb='post', signed=True, params=params) 478 | if not response.get('success'): 479 | logger.error('failed request', extra=response) 480 | return 481 | 482 | return [Withdraw(withdraw) for withdraw in response['withdrawList']] 483 | 484 | def get_deposit_history(self, asset='', **kwargs): 485 | logger = self._logger('get_deposit_history') 486 | 487 | params = {} 488 | if asset: 489 | logger.info(asset) 490 | params['asset'] = asset 491 | 492 | response = self._make_request(Endpoints.DEPOSIT_HISTORY, 493 | verb='post', signed=True, params=params) 494 | if not response.get('success'): 495 | logger.error('failed request', extra=response) 496 | return 497 | 498 | """ TODO 499 | wait for API fix that enforces the `asset` parameter. 500 | Currently it does not, so filter out deposits after 501 | the API call returns. 502 | """ 503 | deposits = [] 504 | if asset: 505 | for deposit in response['depositList']: 506 | if deposit['asset'] == asset: 507 | deposits.append(deposit) 508 | else: 509 | deposits = response['depositList'] 510 | 511 | return [Deposit(d) for d in deposits] 512 | 513 | def event(self, coro): 514 | """ Register a callback function on an event. 515 | 516 | Supported events: 517 | client.on_depth_ready 518 | fires when the initial /depth api call returns. 519 | 520 | client.on_depth_event 521 | fires whenever a @depth websocket event is received. 522 | 523 | client.on_candlesticks_ready 524 | fires when the initial /klines api call returns 525 | 526 | client.on_candlesticks_event 527 | fires whenever a @klines websocket event is received 528 | """ 529 | 530 | if not asyncio.iscoroutinefunction(coro): 531 | raise TypeError('event registered must be a cororoutine function') 532 | 533 | setattr(self, coro.__name__, coro) 534 | -------------------------------------------------------------------------------- /binance/enums.py: -------------------------------------------------------------------------------- 1 | class OrderSides: 2 | BUY = 'BUY' 3 | SELL = 'SELL' 4 | 5 | 6 | class OrderTypes: 7 | MARKET = 'MARKET' 8 | LIMIT = 'LIMIT' 9 | 10 | 11 | class OrderStatus: 12 | NEW = 'NEW' 13 | CANCELED = 'CANCELED' 14 | PARTIALLY_FILLED = 'PARTIALLY_FILLED' 15 | FILLED = 'FILLED' 16 | PENDING_CANCEL = 'PENDING_CANCEL' 17 | REJECTED = 'REJECTED' 18 | EXPIRED = 'EXPIRED' 19 | 20 | 21 | class TimeInForce: 22 | GTC = 'GTC' 23 | IOC = 'IOC' 24 | 25 | 26 | class CandlestickIntervals: 27 | ONE_MINUTE = '1m' 28 | THREE_MINUTE = '3m' 29 | FIVE_MINUTE = '5m' 30 | FIFTEEN_MINUTE = '15m' 31 | THIRTY_MINUTE = '30m' 32 | ONE_HOUR = '1h' 33 | TWO_HOUR = '2h' 34 | FOUR_HOUR = '4h' 35 | SIX_HOUR = '6h' 36 | EIGHT_HOUR = '8h' 37 | TWELVE_HOUR = '12h' 38 | ONE_DAY = '1d' 39 | THREE_DAY = '3d' 40 | ONE_WEEK_ = '1w' 41 | ONE_MONTH = '1M' 42 | -------------------------------------------------------------------------------- /binance/storage.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from datetime import datetime 3 | 4 | from .enums import ( 5 | OrderSides, 6 | OrderTypes, 7 | ) 8 | 9 | 10 | class Ticker: 11 | def __init__(self, raw_ticker): 12 | self.symbol = raw_ticker['symbol'] 13 | self.price = float(raw_ticker['price']) 14 | 15 | def to_json(self): 16 | return deepcopy(self.__dict__) 17 | 18 | 19 | class Account: 20 | def __init__(self, raw_account): 21 | self.maker_commission = raw_account['makerCommission'] 22 | self.taker_commission = raw_account['takerCommission'] 23 | self.buyer_commission = raw_account['buyerCommission'] 24 | self.seller_commission = raw_account['sellerCommission'] 25 | 26 | self.can_trade = raw_account['canTrade'] 27 | self.can_withdraw = raw_account['canWithdraw'] 28 | self.canDeposit = raw_account['canDeposit'] 29 | 30 | self.balances = {} 31 | for balance in raw_account['balances']: 32 | self.balances[balance['asset']] = Balance(balance) 33 | 34 | def to_json(self): 35 | j = deepcopy(self.__dict__) 36 | for asset in self.balances: 37 | j['balances'][asset] = self.banaces[asset].to_json() 38 | 39 | return j 40 | 41 | 42 | class Balance: 43 | def __init__(self, raw_balance): 44 | self.asset = raw_balance['asset'] 45 | self.free = float(raw_balance['free']) 46 | self.locked = float(raw_balance['locked']) 47 | 48 | def to_json(self): 49 | return deepcopy(self.__dict__) 50 | 51 | 52 | class Order: 53 | def __init__(self, raw_order): 54 | self.id = raw_order['orderId'] 55 | self.symbol = raw_order['symbol'] 56 | self.client_order_id = raw_order['clientOrderId'] 57 | self.price = float(raw_order['price']) 58 | self.original_quantity = float(raw_order['origQty']) 59 | self.executed_quantity = float(raw_order['executedQty']) 60 | self.status = raw_order['status'] 61 | self.time_in_force = raw_order['timeInForce'] 62 | self.type = getattr(OrderTypes, raw_order['type']) 63 | self.side = getattr(OrderSides, raw_order['side']) 64 | 65 | self.stop_price = raw_order.get('stopPrice') 66 | if self.stop_price is not None: 67 | self.stop_price = float(self.stop_price) 68 | 69 | self.iceberg_quantity = raw_order.get('icebergQty') 70 | if self.iceberg_quantity is not None: 71 | self.iceberg_quantity = float(self.iceberg_quantity) 72 | 73 | self.time = raw_order.get('time') 74 | if self.time is not None: 75 | self.time = datetime.fromtimestamp(self.time / 1000) 76 | 77 | self.transact_time = raw_order.get('transactTime') 78 | if self.transact_time is not None: 79 | self.transact_time = datetime.fromtimestamp(self.transact_time / 1000) 80 | 81 | def to_json(self): 82 | return deepcopy(self.__dict__) 83 | 84 | 85 | class Trade: 86 | def __init__(self, symbol, raw_trade): 87 | self.symbol = symbol 88 | self.id = raw_trade['id'] 89 | self.price = float(raw_trade['price']) 90 | self.quantity = float(raw_trade['qty']) 91 | self.commission = float(raw_trade['commission']) 92 | self.commission_asset = raw_trade['commissionAsset'] 93 | self.time = raw_trade['time'] 94 | self.isBuyer = raw_trade['isBuyer'] 95 | self.isMaker = raw_trade['isMaker'] 96 | self.isBestMatch = raw_trade['isBestMatch'] 97 | 98 | def to_json(self): 99 | return deepcopy(self.__dict__) 100 | 101 | 102 | class Depth: 103 | def __init__(self, symbol, raw_depth): 104 | self.symbol = symbol 105 | self.update_id = raw_depth['lastUpdateId'] 106 | self.bids = [Bid(b) for b in raw_depth['bids']] 107 | self.asks = [Ask(a) for a in raw_depth['asks']] 108 | 109 | def to_json(self): 110 | return { 111 | 'symbol' : self.symbol, 112 | 'update_id' : self.update_id, 113 | 'bids' : [b.to_json() for b in self.bids], 114 | 'asks' : [a.to_json() for a in self.asks] 115 | } 116 | 117 | 118 | class Bid: 119 | def __init__(self, raw_bid): 120 | self.price = float(raw_bid[0]) 121 | self.quantity = float(raw_bid[1]) 122 | 123 | def to_json(self): 124 | return deepcopy(self.__dict__) 125 | 126 | 127 | class Ask: 128 | def __init__(self, raw_ask): 129 | self.price = float(raw_ask[0]) 130 | self.quantity = float(raw_ask[1]) 131 | 132 | def to_json(self): 133 | return deepcopy(self.__dict__) 134 | 135 | 136 | class Candlestick: 137 | def __init__(self, symbol, raw_candlestick): 138 | self.symbol = symbol 139 | 140 | self.open_time = datetime.fromtimestamp(raw_candlestick[0] / 1000) 141 | self.close_time = datetime.fromtimestamp(raw_candlestick[6] / 1000) 142 | 143 | self.price = CandlestickPrice(*raw_candlestick[1:5]) 144 | self.volume = float(raw_candlestick[5]) 145 | self.quote_asset_volume = float(raw_candlestick[7]) 146 | self.trades = raw_candlestick[8] 147 | self.taker_buy_base_asset_volume = raw_candlestick[9] 148 | self.taker_buy_quote_asset_volume = raw_candlestick[10] 149 | 150 | @classmethod 151 | def from_websocket_event(cls, symbol, event): 152 | transformed_event = [ 153 | event['k']['t'], # open time 154 | event['k']['o'], # open price 155 | event['k']['h'], # high price 156 | event['k']['l'], # low price 157 | event['k']['c'], # close price 158 | event['k']['v'], # volume 159 | event['k']['T'], # close time 160 | event['k']['q'], # quote asset volume ? 161 | event['k']['n'], # trades 162 | event['k']['V'], # taker buy base asset volume ? 163 | event['k']['Q'] # taker buy quote asset volume ? 164 | ] 165 | 166 | return cls(symbol, transformed_event) 167 | 168 | def to_json(self): 169 | j = deepcopy(self.__dict__) 170 | j['open_time'] = self.open_time.timestamp() 171 | j['close_time'] = self.close_time.timestamp() 172 | j['price'] = j['price'].to_json() 173 | 174 | return j 175 | 176 | 177 | class CandlestickPrice: 178 | def __init__(self, open_, high, low, close): 179 | self.open = open_ 180 | self.high = high 181 | self.low = low 182 | self.close = close 183 | 184 | def to_json(self): 185 | return deepcopy(self.__dict__) 186 | 187 | 188 | class Deposit: 189 | def __init__(self, raw_deposit): 190 | self.asset = raw_deposit['asset'] 191 | self.amount = raw_deposit['amount'] 192 | self.status = raw_deposit['status'] 193 | 194 | self.insert_time = raw_deposit.get('insertTime') 195 | if self.insert_time: 196 | self.insert_time = datetime.fromtimestamp(self.insert_time / 1000) 197 | 198 | def to_json(self): 199 | j = deepcopy(self.__dict__) 200 | if self.insert_time: 201 | j['insert_time'] = self.insert_time.timestamp() 202 | 203 | return j 204 | 205 | 206 | class Withdraw: 207 | def __init__(self, raw_withdraw): 208 | self.asset = raw_withdraw['asset'] 209 | self.status = raw_withdraw['status'] 210 | self.amount = raw_withdraw['amount'] 211 | self.address = raw_withdraw['address'] 212 | self.tx_id = raw_withdraw.get('txId') 213 | 214 | apply_time = raw_withdraw['applyTime'] / 1000 215 | self.apply_time = datetime.fromtimestamp(apply_time) 216 | 217 | self.success_time = raw_withdraw.get('successTime') 218 | if self.success_time: 219 | self.success_time = datetime.fromtimestamp(self.success_time / 1000) 220 | 221 | def to_json(self): 222 | j = deepcopy(self.__dict__) 223 | j['apply_time'] = self.apply_time.timestamp() 224 | if self.success_time: 225 | j['success_time'] = self.success_time.timestamp() 226 | 227 | return j 228 | -------------------------------------------------------------------------------- /binance/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from pprint import pprint 4 | import time 5 | 6 | 7 | class GetLoggerMixin: 8 | ''' Adds a `_logger()` classmethod that returns the correctly 9 | named logger. The child class must have a `__loggername__` class variable. 10 | ''' 11 | 12 | @classmethod 13 | def _logger(cls, name=''): 14 | logger_name = cls.__loggername__ 15 | if name: 16 | logger_name += f'.{name}' 17 | 18 | return logging.getLogger(logger_name) 19 | 20 | 21 | def pp(o): 22 | try: 23 | print(json.dumps(o, indent=2, sort_keys=True)) 24 | except: 25 | pprint(o) 26 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | main: 2 | apikey: 3 | apisecret: 4 | 5 | logging: 6 | version: 1 7 | 8 | formatters: 9 | simple: 10 | format: "%(asctime)s [%(levelname)-5.5s][%(name)s] %(msg)s" 11 | 12 | handlers: 13 | console: 14 | formatter: simple 15 | class: logging.StreamHandler 16 | 17 | root: 18 | level: DEBUG 19 | handlers: 20 | - console 21 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests/test_fetches.py 3 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c0lon/binance-api-python/419bbadbb8e19a563c6e012a60961db7668a5731/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/watch_candlesticks.py: -------------------------------------------------------------------------------- 1 | """ Watch the candlesticks of a given symbol. 2 | """ 3 | 4 | 5 | from datetime import datetime 6 | import signal 7 | import sys 8 | 9 | from binance import ( 10 | BinanceClient, 11 | configure_app, 12 | get_default_arg_parser, 13 | ) 14 | 15 | 16 | def quit_handler(signum, frame): 17 | sys.exit(0) 18 | signal.signal(signal.SIGINT, quit_handler) 19 | signal.signal(signal.SIGTERM, quit_handler) 20 | 21 | 22 | def main(): 23 | arg_parser = get_default_arg_parser() 24 | arg_parser.add_argument('symbol', type=str, 25 | help='watch the candlesticks of symbol .') 26 | arg_parser.add_argument('interval', type=str, 27 | help='set the candlesticks interval.') 28 | arg_parser.add_argument('-d', '--depth', type=int, 29 | help='display the latest candlesticks.') 30 | 31 | settings, config = configure_app(arg_parser=arg_parser) 32 | symbol = config['args']['symbol'] 33 | interval = config['args']['interval'] 34 | depth = config['args']['depth'] 35 | 36 | client = BinanceClient(settings['apikey'], settings['apisecret']) 37 | 38 | @client.event 39 | async def on_candlesticks_ready(): 40 | """ This coroutine runs when the inital /candlesticks API call returns. 41 | """ 42 | print('candlesticks ready') 43 | client.candlestick_cache[(symbol, interval)].pretty_print(depth) 44 | 45 | @client.event 46 | async def on_candlesticks_event(event): 47 | """ This coroutine runs whenever a @candlesticks websocket event is received. 48 | """ 49 | cache = client.candlestick_cache[(symbol, interval)] 50 | latest_candlestick = cache.candlesticks[-1] 51 | date_string = latest_candlestick.open_time.strftime('%Y-%m-%d %H:%M:%S') 52 | event_date_string = datetime.fromtimestamp(event['E'] / 1000) 53 | print(f'UPDATE {event_date_string}\n') 54 | print(f'{latest_candlestick.symbol} {date_string}') 55 | print(f' open: {latest_candlestick.price.open}') 56 | print(f' high: {latest_candlestick.price.high}') 57 | print(f' low: {latest_candlestick.price.low}') 58 | print(f' close: {latest_candlestick.price.low}') 59 | print(f' volume: {latest_candlestick.volume}') 60 | print() 61 | 62 | client.watch_candlesticks(symbol, interval) 63 | 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /scripts/watch_depth.py: -------------------------------------------------------------------------------- 1 | """ Watch the depth of a given symbol. 2 | """ 3 | 4 | 5 | import signal 6 | import sys 7 | 8 | from binance import ( 9 | BinanceClient, 10 | configure_app, 11 | get_default_arg_parser, 12 | ) 13 | 14 | 15 | def quit_handler(signum, frame): 16 | sys.exit(0) 17 | signal.signal(signal.SIGINT, quit_handler) 18 | signal.signal(signal.SIGTERM, quit_handler) 19 | 20 | 21 | def main(): 22 | arg_parser = get_default_arg_parser() 23 | arg_parser.add_argument('symbol', type=str, 24 | help='watch the depth of symbol .') 25 | arg_parser.add_argument('-l', '--depth-limit', type=int, 26 | help='show the latest orders on each side.') 27 | 28 | settings, config = configure_app(arg_parser=arg_parser) 29 | symbol = config['args']['symbol'] 30 | depth_limit = config['args']['depth_limit'] 31 | 32 | client = BinanceClient(settings['apikey'], settings['apisecret']) 33 | 34 | @client.event 35 | async def on_depth_ready(depth): 36 | """ This coroutine runs when the inital /depth API call returns. 37 | """ 38 | print('depth ready') 39 | client.depth_cache[symbol].pretty_print(depth_limit) 40 | 41 | @client.event 42 | async def on_depth_event(event): 43 | """ This coroutine runs whenever a @depth websocket event is received. 44 | """ 45 | print(f'update id: {event["u"]}') # print the event id 46 | client.depth_cache[symbol].pretty_print(depth_limit) 47 | 48 | client.watch_depth(symbol) 49 | 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import ( 3 | find_packages, 4 | setup, 5 | ) 6 | 7 | 8 | here = os.path.dirname(os.path.realpath(__file__)) 9 | with open(os.path.join(here, 'README.md')) as f: 10 | README = f.read().strip() 11 | with open(os.path.join(here, 'CHANGES.md')) as f: 12 | CHANGES = f.read().strip() 13 | with open(os.path.join(here, 'VERSION')) as f: 14 | VERSION = f.read().strip() 15 | 16 | dependency_links = [] 17 | setup_requires = [ 18 | 'pytest-runner', 19 | ] 20 | install_requires = [ 21 | 'aiohttp', 22 | 'pyyaml', 23 | 'requests', 24 | 'websockets', 25 | ] 26 | test_requires = [ 27 | 'pytest', 28 | ] 29 | 30 | data_files = [ 31 | ('', ['README.md', 'CHANGES.md', 'VERSION']), 32 | ] 33 | entry_points = { 34 | 'console_scripts': [ 35 | 'watchdepth = scripts.watch_depth:main', 36 | 'watchcandlesticks = scripts.watch_candlesticks:main', 37 | ] 38 | } 39 | 40 | setup(name='binance', 41 | description='Binance API Application', 42 | long_description=README + '\n\n' + CHANGES, 43 | version=VERSION, 44 | author='c0lon', 45 | author_email='', 46 | dependency_links=dependency_links, 47 | setup_requires=setup_requires, 48 | install_requires=install_requires, 49 | test_requires=test_requires, 50 | packages=find_packages(), 51 | data_files=data_files, 52 | include_package_data=True, 53 | entry_points=entry_points 54 | ) 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from binance import ( 4 | BinanceClient, 5 | configure_app, 6 | ) 7 | 8 | 9 | TEST_CONFIG_FILE = 'config.yaml' 10 | 11 | root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 12 | config_uri = os.path.join(root, TEST_CONFIG_FILE) 13 | SETTINGS, GLOBAL_CONFIG = configure_app(config_uri=config_uri) 14 | 15 | APIKEY = SETTINGS['apikey'] 16 | APISECRET = SETTINGS['apisecret'] 17 | CLIENT = BinanceClient(APIKEY, APISECRET) 18 | 19 | ASSETS = [ 20 | 'BTC', 21 | 'ETH', 22 | ] 23 | SYMBOLS = [ 24 | 'ETHBTC', 25 | 'LTCBTC', 26 | 'BNBBTC', 27 | 'NEOBTC', 28 | 'OMGBTC', 29 | 'WTCBTC', 30 | ] 31 | -------------------------------------------------------------------------------- /tests/test_actions.py: -------------------------------------------------------------------------------- 1 | """ 2 | +-------------------------------+ 3 | |TEST CONFIGURATION INSTRUCTIONS| 4 | +-------------------------------+ 5 | 6 | All tests in this file contain variables that will be passed 7 | as parameters to their respective API endpoints. Set these 8 | variables so that you can observe that the tests perform the 9 | actions you expect. 10 | 11 | +---------------+ 12 | |!!! WARNING !!!| 13 | +---------------+ 14 | Your account balances WILL CHANGE if these tests are successful. 15 | Make sure you understand what these tests will do, and that you 16 | are okay with the results. 17 | """ 18 | 19 | 20 | import time 21 | 22 | import pytest 23 | 24 | from . import CLIENT 25 | from binance.storage import * 26 | from binance.enums import OrderStatus 27 | 28 | 29 | """ 30 | TEST WITHDRAW 31 | 32 | Set `asset` to the asset you wish to withdraw. 33 | Set `amount` to the amount of `asset` to withdraw. 34 | Set `address` to the address to which you wish to 35 | withdraw your `asset`. 36 | 37 | RESULTS IF SUCCESSFUL: 38 | - Your account will initiate a withdraw `amount` of 39 | `asset` to `address`. 40 | 41 | CONSIDERATIONS: 42 | Make sure you have at least `quantity` of `asset` in 43 | your account, or the test will fail. 44 | """ 45 | 46 | #@pytest.mark.skip 47 | def test_withdraw(): 48 | asset = '' 49 | amount = 0.0 50 | address = '' 51 | 52 | withdraw = CLIENT.withdraw(asset, amount, address) 53 | assert withdraw 54 | 55 | 56 | """ 57 | TEST MARKET BUY ORDER 58 | 59 | Set `symbol` to the pair you wish to buy at market price. 60 | Set `quantity` to the quantity of your desired market buy. 61 | 62 | RESULTS IF SUCCESSFUL: 63 | - Your account will have place a MARKET BUY ORDER 64 | for `quantity` of `asset`. 65 | 66 | CONSIDERATIONS: 67 | Make sure you have enough funds for this transaction, 68 | otherwise the test will fail. 69 | 70 | Since it's a market order, it will likely be filled. 71 | Make sure you are okay with this. 72 | """ 73 | 74 | #@pytest.mark.skip 75 | def test_place_market_buy(): 76 | symbol = '' 77 | quantity = 0.0 78 | 79 | order = CLIENT.place_market_buy(symbol, quantity) 80 | assert isinstance(order, Order) 81 | assert order.symbol == symbol 82 | assert order.original_quantity == quantity 83 | 84 | 85 | """ 86 | TEST MARKET SELL ORDER 87 | 88 | Set `symbol` to the pair you wish to buy at market price. 89 | Set `quantity` to the quantity of your desired market sell. 90 | 91 | RESULTS IF SUCCESSFUL: 92 | - Your account will have place a MARKET SELL ORDER 93 | for `quantity` of `asset`. 94 | 95 | CONSIDERATIONS: 96 | Make sure you have enough funds for this transaction, 97 | otherwise the test will fail. 98 | 99 | Since it's a market order, it will likely be filled. 100 | Make sure you are okay with this. 101 | """ 102 | 103 | #@pytest.mark.skip 104 | def test_place_market_sell(): 105 | symbol = '' 106 | quantity = 0.0 107 | 108 | order = CLIENT.place_market_sell(symbol, quantity) 109 | assert isinstance(order, Order) 110 | assert order.symbol == symbol 111 | assert order.original_quantity == quantity 112 | 113 | 114 | """ 115 | TEST LIMIT BUY 116 | 117 | Set `symbol` to the pair you wish to buy at a given price. 118 | Set `quantity` to the quantity of your desired limit sell. 119 | Set `price` to the price you at which wish you sell your `symbol`. 120 | 121 | RESULTS IF SUCCESSFUL: 122 | - Your account will have placed a MARKET BUY ORDER 123 | for `quantity` of `symbol` at `price`. 124 | 125 | CONSIDERATIONS: 126 | If you don't want any actual changes to be made, set 127 | `price` to something that you are sure will never be filled. 128 | 129 | Otherwise, make sure you have enough funds to handle the 130 | transaction. If not, the test will fail. 131 | 132 | If the test succeeds, be sure to cancel the order 133 | if you don't want it to be filled. 134 | """ 135 | 136 | #@pytest.mark.skip 137 | def test_place_limit_buy(): 138 | symbol = '' 139 | quantity = 0.0 140 | price = 0.0 141 | 142 | order = CLIENT.place_limit_buy(symbol, quantity, price) 143 | assert isinstance(order, Order) 144 | assert order.symbol == symbol 145 | assert order.original_quantity == quantity 146 | assert order.price == price 147 | 148 | 149 | """ 150 | TEST LIMIT SELL 151 | 152 | Set `symbol` to the pair you wish to sell at a given price. 153 | Set `quantity` to the quantity of your desired limit sell. 154 | Set `price` to the price you at which wish you sell your `symbol`. 155 | 156 | RESULTS IF SUCCESSFUL: 157 | - Your account will have placed a MARKET SELL ORDER 158 | for `quantity` of `symbol` at `price`. 159 | 160 | CONSIDERATIONS: 161 | If you don't want any actual changes to be made, set 162 | `price` to something that you are sure will never be filled. 163 | 164 | Otherwise, make sure you have enough funds to handle the 165 | transaction. If not, the test will fail. 166 | 167 | If the test succeeds, be sure to cancel the order 168 | if you don't want it to be filled. 169 | """ 170 | 171 | #@pytest.mark.skip 172 | def test_place_limit_sell(): 173 | symbol = '' 174 | quantity = 0.0 175 | price = 0.0 176 | 177 | order = CLIENT.place_limit_sell(symbol, quantity, price) 178 | assert isinstance(order, Order) 179 | assert order.symbol == symbol 180 | assert order.original_quantity == quantity 181 | assert order.price == price 182 | 183 | 184 | """ 185 | TEST ORDER STATUS + ORDER CANCEL 186 | 187 | This test places a LIMIT SELL ORDER, checks its status, 188 | then cancels the order. 189 | 190 | Set `symbol` to the pair you wish to sell at a given price. 191 | Set `quantity` to the quantity of your desired limit sell. 192 | Set `price` to the price at which you wish to sell your `symbol`. 193 | 194 | RESULTS IF SUCCESSFUL: 195 | - Your account will place a MARKET SELL ORDER for `quantity` of 196 | `symbol` at `price`. 197 | - The API CLIENT will check the status of the order. 198 | - Your account will cancel the order. 199 | - The API CLIENT wil once again check the status of the order, 200 | and ensure that it was cancelled. 201 | 202 | CONSIDERATIONS: 203 | Set `price` to something such that you are sure that the 204 | order won't be filled immediately. This way, the client 205 | has time to cancel the order. 206 | 207 | If the order is filled before the client can cancel it, 208 | the test will fail. 209 | """ 210 | 211 | #@pytest.mark.skip 212 | def test_check_order_status_and_cancel(): 213 | symbol = '' 214 | quantity = 0.0 215 | price = 0.0 216 | 217 | order = CLIENT.place_limit_sell(symbol, quantity, price) 218 | 219 | time.sleep(1) 220 | 221 | order_status = CLIENT.get_order_status(symbol, order.id) 222 | assert order_status.id == order.id 223 | assert order_status.status == OrderStatus.NEW 224 | 225 | assert CLIENT.cancel_order(symbol, order.id) 226 | 227 | order_status = CLIENT.get_order_status(symbol, order.id) 228 | assert order_status.id == order.id 229 | assert order_status.status == OrderStatus.CANCELED 230 | -------------------------------------------------------------------------------- /tests/test_fetches.py: -------------------------------------------------------------------------------- 1 | """ Test suite for the Binance API Client. 2 | """ 3 | 4 | 5 | import asyncio 6 | from datetime import datetime 7 | import json 8 | import os 9 | import pytest 10 | import random 11 | 12 | from . import ( 13 | ASSETS, 14 | CLIENT, 15 | SYMBOLS, 16 | ) 17 | from binance.enums import ( 18 | CandlestickIntervals, 19 | OrderStatus, 20 | ) 21 | from binance.storage import * 22 | 23 | 24 | #@pytest.mark.skip 25 | def test_ping(): 26 | assert CLIENT.ping() 27 | 28 | 29 | #@pytest.mark.skip 30 | def test_get_server_time(): 31 | assert isinstance(CLIENT.get_server_time(), int) 32 | 33 | 34 | def assert_ticker(ticker): 35 | assert isinstance(ticker, Ticker) 36 | assert isinstance(ticker.symbol, str) 37 | assert isinstance(ticker.price, float) 38 | 39 | ticker_json = ticker.to_json() 40 | assert ticker_json['symbol'] == ticker.symbol 41 | assert ticker_json['price'] == ticker.price 42 | 43 | 44 | #@pytest.mark.skip 45 | def test_get_ticker(): 46 | tickers = CLIENT.get_ticker() 47 | 48 | assert isinstance(tickers, list) 49 | symbols = set() 50 | for ticker in tickers: 51 | assert_ticker(ticker) 52 | symbols.add(ticker.symbol) 53 | 54 | for symbol in SYMBOLS: 55 | assert symbol in symbols 56 | 57 | 58 | #@pytest.mark.skip 59 | def test_get_ticker_symbol(): 60 | symbol = random.choice(SYMBOLS) 61 | ticker = CLIENT.get_ticker(symbol) 62 | 63 | assert ticker.symbol == symbol 64 | assert_ticker(ticker) 65 | 66 | 67 | #@pytest.mark.skip 68 | def test_get_ticker_invalid(): 69 | symbol = 'DOGE' 70 | 71 | try: 72 | ticker = CLIENT.get_ticker(symbol) 73 | except ValueError as e: 74 | assert e.args[0] == f'invalid symbol: {symbol}' 75 | else: 76 | assert False 77 | 78 | 79 | def assert_candlestick(candlestick): 80 | assert isinstance(candlestick, Candlestick) 81 | assert isinstance(candlestick.price, CandlestickPrice) 82 | assert candlestick.price.high >= candlestick.price.low 83 | 84 | assert isinstance(candlestick.open_time, datetime) 85 | assert isinstance(candlestick.close_time, datetime) 86 | assert candlestick.close_time > candlestick.open_time 87 | 88 | candlestick_json = candlestick.to_json() 89 | assert candlestick_json['price'] == candlestick.price.to_json() 90 | assert datetime.fromtimestamp(candlestick_json['open_time']) == candlestick.open_time 91 | assert datetime.fromtimestamp(candlestick_json['close_time']) == candlestick.close_time 92 | 93 | 94 | #@pytest.mark.skip 95 | def test_get_candlesticks(): 96 | symbol = random.choice(SYMBOLS) 97 | candlesticks = CLIENT.get_candlesticks(symbol, 98 | CandlestickIntervals.THIRTY_MINUTE) 99 | 100 | for candlestick in candlesticks: 101 | assert_candlestick(candlestick) 102 | 103 | 104 | #@pytest.mark.skip 105 | def test_get_candlesticks_async(): 106 | symbol = random.choice(SYMBOLS) 107 | 108 | async def candlesticks_callback(candlesticks): 109 | candlesticks_json = [c.to_json() for c in candlesticks] 110 | with open('candlesticks.json', 'w+') as f: 111 | json.dump(candlesticks_json, f) 112 | 113 | async def get_candlesticks(): 114 | candlesticks = await CLIENT.get_candlesticks_async(symbol, 115 | CandlestickIntervals.ONE_HOUR, callback=candlesticks_callback) 116 | for candlestick in candlesticks: 117 | assert candlestick.symbol == symbol 118 | assert_candlestick(candlestick) 119 | 120 | loop = asyncio.get_event_loop() 121 | loop.run_until_complete(get_candlesticks()) 122 | 123 | with open('candlesticks.json') as f: 124 | candlesticks_json = json.load(f) 125 | for candlestick in candlesticks_json: 126 | assert candlestick['symbol'] == symbol 127 | os.remove('candlesticks.json') 128 | 129 | 130 | def assert_depth(depth): 131 | assert isinstance(depth, Depth) 132 | assert isinstance(depth.update_id, int) 133 | 134 | for bid in depth.bids: 135 | assert isinstance(bid, Bid) 136 | assert isinstance(bid.price, float) 137 | assert isinstance(bid.quantity, float) 138 | 139 | for ask in depth.asks: 140 | assert isinstance(ask, Ask) 141 | assert isinstance(ask.price, float) 142 | assert isinstance(ask.quantity, float) 143 | 144 | depth_json = depth.to_json() 145 | assert depth_json['symbol'] == depth.symbol 146 | assert depth_json['update_id'] == depth.update_id 147 | for i, bid_json in enumerate(depth_json['bids']): 148 | assert bid_json == depth.bids[i].to_json() 149 | for i, ask_json in enumerate(depth_json['asks']): 150 | assert ask_json == depth.asks[i].to_json() 151 | 152 | 153 | #@pytest.mark.skip 154 | def test_get_depth_data(): 155 | symbol = random.choice(SYMBOLS) 156 | depth = CLIENT.get_depth(symbol) 157 | 158 | assert depth.symbol == symbol 159 | assert_depth(depth) 160 | 161 | 162 | #@pytest.mark.skip 163 | def test_get_depth_data_async(): 164 | symbol = random.choice(SYMBOLS) 165 | 166 | async def depth_callback(depth): 167 | with open('depth.json', 'w+') as f: 168 | json.dump(depth.to_json(), f) 169 | 170 | async def get_depth(): 171 | depth = await CLIENT.get_depth_async(symbol, callback=depth_callback) 172 | assert depth.symbol == symbol 173 | assert_depth(depth) 174 | 175 | loop = asyncio.get_event_loop() 176 | loop.run_until_complete(get_depth()) 177 | 178 | with open('depth.json') as f: 179 | depth_json = json.load(f) 180 | assert depth_json['symbol'] == symbol 181 | assert isinstance(depth_json['update_id'], int) 182 | assert isinstance(depth_json['bids'], list) 183 | assert isinstance(depth_json['asks'], list) 184 | os.remove('depth.json') 185 | 186 | 187 | #@pytest.mark.skip 188 | def test_get_account_info(): 189 | account = CLIENT.get_account_info() 190 | assert isinstance(account, Account) 191 | 192 | for asset, balance in account.balances.items(): 193 | assert isinstance(balance, Balance) 194 | assert balance.asset == asset 195 | 196 | balance_json = balance.to_json() 197 | assert balance_json['asset'] == balance.asset 198 | assert balance_json['free'] == balance.free 199 | assert balance_json['locked'] == balance.locked 200 | 201 | 202 | #@pytest.mark.skip 203 | def test_get_trade_info(): 204 | symbol = random.choice(SYMBOLS) 205 | trade_info = CLIENT.get_trade_info(symbol) 206 | 207 | assert isinstance(trade_info, list) 208 | for trade in trade_info: 209 | assert isinstance(trade, Trade) 210 | assert trade.symbol == symbol 211 | 212 | 213 | #@pytest.mark.skip 214 | def test_get_open_orders(): 215 | symbol = random.choice(SYMBOLS) 216 | open_orders = CLIENT.get_open_orders(symbol) 217 | 218 | assert isinstance(open_orders, list) 219 | for order in open_orders: 220 | assert isinstance(order, Order) 221 | assert order.symbol == symbol 222 | 223 | 224 | #@pytest.mark.skip 225 | def test_get_all_orders(): 226 | symbol = random.choice(SYMBOLS) 227 | orders = CLIENT.get_all_orders(symbol) 228 | 229 | assert isinstance(orders, list) 230 | for order in orders: 231 | assert isinstance(order, Order) 232 | assert order.symbol == symbol 233 | 234 | 235 | def assert_withdraw(withdraw): 236 | assert isinstance(withdraw, Withdraw) 237 | assert isinstance(withdraw.apply_time, datetime) 238 | if withdraw.success_time: 239 | assert isinstance(withdraw.success_time, datetime) 240 | assert withdraw.tx_id 241 | 242 | withdraw_json = withdraw.to_json() 243 | assert withdraw_json['asset'] == withdraw.asset 244 | assert datetime.fromtimestamp(withdraw_json['apply_time']) == withdraw.apply_time 245 | if withdraw.success_time: 246 | assert datetime.fromtimestamp(withdraw_json['success_time']) == withdraw.success_time 247 | 248 | 249 | #@pytest.mark.skip 250 | def test_get_withdraw_history(): 251 | history = CLIENT.get_withdraw_history() 252 | for withdraw in history: 253 | assert_withdraw(withdraw) 254 | 255 | 256 | #@pytest.mark.skip 257 | def test_get_withdraw_history_asset(): 258 | asset = random.choice(ASSETS) 259 | 260 | history = CLIENT.get_withdraw_history(asset) 261 | for withdraw in history: 262 | assert withdraw.asset == asset 263 | assert_withdraw(withdraw) 264 | 265 | 266 | def assert_deposit(deposit): 267 | assert isinstance(deposit, Deposit) 268 | if deposit.insert_time: 269 | assert isinstance(deposit.insert_time, datetime) 270 | 271 | deposit_json = deposit.to_json() 272 | assert deposit_json['asset'] == deposit.asset 273 | assert deposit_json['amount'] == deposit.amount 274 | assert deposit_json['status'] == deposit.status 275 | if deposit.insert_time: 276 | assert datetime.fromtimestamp(deposit_json['insert_time']) == deposit.insert_time 277 | 278 | 279 | #@pytest.mark.skip 280 | def test_get_deposit_history(): 281 | history = CLIENT.get_deposit_history() 282 | for deposit in history: 283 | assert_deposit(deposit) 284 | 285 | 286 | #@pytest.mark.skip 287 | def test_get_deposit_history_asset(): 288 | asset = random.choice(ASSETS) 289 | 290 | history = CLIENT.get_deposit_history(asset) 291 | for deposit in history: 292 | assert deposit.asset == asset 293 | assert_deposit(deposit) 294 | --------------------------------------------------------------------------------