├── .gitignore ├── LICENSE ├── README.md ├── check_code.sh ├── currencycom ├── __init__.py └── client.py ├── integration_tests ├── __init__.py ├── rest │ ├── README.md │ ├── __init__.py │ ├── conftest.py │ └── test_account.py └── wss │ └── __init__.py ├── push_new_version.sh ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py └── test_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | ### VirtualEnv template 93 | # Virtualenv 94 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 95 | .Python 96 | [Bb]in 97 | [Ii]nclude 98 | [Ll]ib 99 | [Ll]ib64 100 | [Ll]ocal 101 | [Ss]cripts 102 | pyvenv.cfg 103 | .venv 104 | pip-selfcheck.json 105 | ### JetBrains template 106 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 107 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 108 | 109 | # User-specific stuff: 110 | .idea/workspace.xml 111 | .idea/tasks.xml 112 | .idea/dictionaries 113 | .idea/vcs.xml 114 | .idea/jsLibraryMappings.xml 115 | 116 | # Sensitive or high-churn files: 117 | .idea/dataSources.ids 118 | .idea/dataSources.xml 119 | .idea/dataSources.local.xml 120 | .idea/sqlDataSources.xml 121 | .idea/dynamic.xml 122 | .idea/uiDesigner.xml 123 | 124 | # Gradle: 125 | .idea/gradle.xml 126 | .idea/libraries 127 | 128 | # Mongo Explorer plugin: 129 | .idea/mongoSettings.xml 130 | 131 | .idea/ 132 | 133 | ## File-based project format: 134 | *.iws 135 | 136 | ## Plugin-specific files: 137 | 138 | # IntelliJ 139 | /out/ 140 | 141 | # mpeltonen/sbt-idea plugin 142 | .idea_modules/ 143 | 144 | # JIRA plugin 145 | atlassian-ide-plugin.xml 146 | 147 | # Crashlytics plugin (for Android Studio and IntelliJ) 148 | com_crashlytics_export_strings.xml 149 | crashlytics.properties 150 | crashlytics-build.properties 151 | fabric.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aliaksandr Sheliutsin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Welcome to the python-currencycom 2 | 3 | This is an unofficial Python wrapper for the Currency.com exchange REST API v1. 4 | I am in no way affiliated with Currency.com, use at your own risk. 5 | 6 | ### Documentation 7 | Please find official documentation by: 8 | https://exchange.currency.com/api 9 | 10 | Or Swagger by the link: https://apitradedoc.currency.com/swagger-ui.html#/. 11 | 12 | 13 | ### QuickStart 14 | 15 | [Register an account on Currency.com](https://exchange.currency.com/trading/signup) 16 | 17 | [Create an API Key with correct permissions](https://exchange.currency.com/trading/platform/settings) 18 | 19 | ``` 20 | pip install python-currencycom 21 | ``` 22 | 23 | Let's retrieve tradable symbols on the market 24 | ```python 25 | from pprint import pprint 26 | 27 | from currencycom.client import Client 28 | 29 | client = Client('API_KEY', 'SECRET_KEY') 30 | 31 | # Exchange info contains various info including tradable symbols 32 | exchange_info = client.get_exchange_info() 33 | tradable_symbols = [x['symbol'] for x in exchange_info['symbols']] 34 | pprint(tradable_symbols, 35 | indent=2) 36 | ``` 37 | 38 | For more check out [the documentation](https://exchange.currency.com/api) and [Swagger](https://apitradedoc.currency.com/swagger-ui.html#/). 39 | -------------------------------------------------------------------------------- /check_code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e +x 3 | #set +x 4 | 5 | echo "Running tests" 6 | pytest ./tests 7 | 8 | echo "Running check for style guide PEP8" 9 | flake8 currencycom/ 10 | 11 | echo "DONE!" 12 | #twine upload -------------------------------------------------------------------------------- /currencycom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sann05/python-currencycom/d790bd008e1cc83e64df3686f5b65ba751d9772a/currencycom/__init__.py -------------------------------------------------------------------------------- /currencycom/client.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | from datetime import datetime, timedelta 4 | from enum import Enum 5 | 6 | import requests 7 | from requests.models import RequestEncodingMixin 8 | 9 | 10 | class CurrencyComConstants(object): 11 | HEADER_API_KEY_NAME = 'X-MBX-APIKEY' 12 | API_VERSION = 'v1' 13 | BASE_URL = 'https://api-adapter.backend.currency.com/api/{}/'.format( 14 | API_VERSION 15 | ) 16 | 17 | AGG_TRADES_MAX_LIMIT = 1000 18 | KLINES_MAX_LIMIT = 1000 19 | RECV_WINDOW_MAX_LIMIT = 60000 20 | 21 | # Public API Endpoints 22 | SERVER_TIME_ENDPOINT = BASE_URL + 'time' 23 | EXCHANGE_INFORMATION_ENDPOINT = BASE_URL + 'exchangeInfo' 24 | 25 | # Market data Endpoints 26 | ORDER_BOOK_ENDPOINT = BASE_URL + 'depth' 27 | AGGREGATE_TRADE_LIST_ENDPOINT = BASE_URL + 'aggTrades' 28 | KLINES_DATA_ENDPOINT = BASE_URL + 'klines' 29 | PRICE_CHANGE_24H_ENDPOINT = BASE_URL + 'ticker/24hr' 30 | 31 | # Account Endpoints 32 | ACCOUNT_INFORMATION_ENDPOINT = BASE_URL + 'account' 33 | ACCOUNT_TRADE_LIST_ENDPOINT = BASE_URL + 'myTrades' 34 | 35 | # Order Endpoints 36 | ORDER_ENDPOINT = BASE_URL + 'order' 37 | CURRENT_OPEN_ORDERS_ENDPOINT = BASE_URL + 'openOrders' 38 | 39 | # Leverage Endpoints 40 | CLOSE_TRADING_POSITION_ENDPOINT = BASE_URL + 'closeTradingPosition' 41 | TRADING_POSITIONS_ENDPOINT = BASE_URL + 'tradingPositions' 42 | LEVERAGE_SETTINGS_ENDPOINT = BASE_URL + 'leverageSettings' 43 | UPDATE_TRADING_ORDERS_ENDPOINT = BASE_URL + 'updateTradingOrder' 44 | UPDATE_TRADING_POSITION_ENDPOINT = BASE_URL + 'updateTradingPosition' 45 | 46 | 47 | class OrderStatus(Enum): 48 | NEW = 'NEW' 49 | FILLED = 'FILLED' 50 | CANCELED = 'CANCELED' 51 | REJECTED = 'REJECTED' 52 | 53 | 54 | class OrderType(Enum): 55 | LIMIT = 'LIMIT' 56 | MARKET = 'MARKET' 57 | STOP = 'STOP' 58 | 59 | 60 | class OrderSide(Enum): 61 | BUY = 'BUY' 62 | SELL = 'SELL' 63 | 64 | 65 | class CandlesticksChartInervals(Enum): 66 | MINUTE = '1m' 67 | FIVE_MINUTES = '5m' 68 | FIFTEEN_MINUTES = '15m' 69 | THIRTY_MINUTES = '30m' 70 | HOUR = '1h' 71 | FOUR_HOURS = '4h' 72 | DAY = '1d' 73 | WEEK = '1w' 74 | 75 | 76 | class TimeInForce(Enum): 77 | GTC = 'GTC' 78 | 79 | 80 | class NewOrderResponseType(Enum): 81 | ACK = 'ACK' 82 | RESULT = 'RESULT' 83 | FULL = 'FULL' 84 | 85 | 86 | class Client(object): 87 | """ 88 | This is API for market Currency.com 89 | Please find documentation by https://exchange.currency.com/api 90 | Swagger UI: https://apitradedoc.currency.com/swagger-ui.html#/ 91 | """ 92 | 93 | def __init__(self, api_key, api_secret): 94 | self.api_key = api_key 95 | self.api_secret = bytes(api_secret, 'utf-8') 96 | 97 | @staticmethod 98 | def _validate_limit(limit): 99 | max_limit = 1000 100 | valid_limits = [5, 10, 20, 50, 100, 500, 1000, 5000] 101 | if limit > max_limit: 102 | raise ValueError('Limit {} more than max limit: {}'.format( 103 | limit, max_limit 104 | )) 105 | if limit not in valid_limits: 106 | raise ValueError('Limit {} not among acceptable values: {}'.format( 107 | limit, valid_limits 108 | )) 109 | 110 | @staticmethod 111 | def _to_epoch_miliseconds(dttm: datetime): 112 | if dttm: 113 | return int(dttm.timestamp() * 1000) 114 | else: 115 | return dttm 116 | 117 | def _validate_recv_window(self, recv_window): 118 | max_value = CurrencyComConstants.RECV_WINDOW_MAX_LIMIT 119 | if recv_window and recv_window > max_value: 120 | raise ValueError( 121 | 'recvValue cannot be greater than {}. Got {}.'.format( 122 | max_value, 123 | recv_window 124 | )) 125 | 126 | @staticmethod 127 | def _validate_new_order_resp_type( 128 | new_order_resp_type: NewOrderResponseType, 129 | order_type: OrderType 130 | ): 131 | if new_order_resp_type == NewOrderResponseType.ACK: 132 | raise ValueError('ACK mode no more available') 133 | 134 | if order_type == OrderType.MARKET: 135 | if new_order_resp_type not in [NewOrderResponseType.RESULT, 136 | NewOrderResponseType.FULL]: 137 | raise ValueError( 138 | "new_order_resp_type for MARKET order can be only RESULT" 139 | f"or FULL. Got {new_order_resp_type.value}") 140 | elif order_type == OrderType.LIMIT: 141 | if new_order_resp_type != NewOrderResponseType.RESULT: 142 | raise ValueError( 143 | "new_order_resp_type for LIMIT order can be only RESULT." 144 | f" Got {new_order_resp_type.value}") 145 | 146 | def _get_params_with_signature(self, **kwargs): 147 | t = self._to_epoch_miliseconds(datetime.now()) 148 | kwargs['timestamp'] = t 149 | # pylint: disable=no-member 150 | body = RequestEncodingMixin._encode_params(kwargs) 151 | sign = hmac.new(self.api_secret, bytes(body, 'utf-8'), 152 | hashlib.sha256).hexdigest() 153 | return {'signature': sign, **kwargs} 154 | 155 | def _get_header(self, **kwargs): 156 | return { 157 | **kwargs, 158 | CurrencyComConstants.HEADER_API_KEY_NAME: self.api_key 159 | } 160 | 161 | def _get(self, url, **kwargs): 162 | return requests.get(url, 163 | params=self._get_params_with_signature(**kwargs), 164 | headers=self._get_header()) 165 | 166 | def _post(self, url, **kwargs): 167 | return requests.post(url, 168 | params=self._get_params_with_signature(**kwargs), 169 | headers=self._get_header()) 170 | 171 | def _delete(self, url, **kwargs): 172 | return requests.delete(url, 173 | params=self._get_params_with_signature( 174 | **kwargs), 175 | headers=self._get_header()) 176 | 177 | def get_account_info(self, 178 | show_zero_balance: bool = False, 179 | recv_window: int = None): 180 | """ 181 | Get current account information 182 | 183 | :param show_zero_balance: will or will not show accounts with zero 184 | balances. Default value False 185 | :param recv_window: the value cannot be greater than 60000 186 | Default value 5000 187 | :return: dict object 188 | Response: 189 | { 190 | "makerCommission":0.20, 191 | "takerCommission":0.20, 192 | "buyerCommission":0.20, 193 | "sellerCommission":0.20, 194 | "canTrade":true, 195 | "canWithdraw":true, 196 | "canDeposit":true, 197 | "updateTime":1586935521, 198 | "balances":[ 199 | { 200 | "accountId":"2376104765040206", 201 | "collateralCurrency":true, 202 | "asset":"BYN", 203 | "free":0.0, 204 | "locked":0.0, 205 | "default":false 206 | }, 207 | { 208 | "accountId":"2376109060084932", 209 | "collateralCurrency":true, 210 | "asset":"USD", 211 | "free":515.59092523, 212 | "locked":0.0, 213 | "default":true 214 | } 215 | ] 216 | } 217 | """ 218 | self._validate_recv_window(recv_window) 219 | r = self._get(CurrencyComConstants.ACCOUNT_INFORMATION_ENDPOINT, 220 | showZeroBalance=show_zero_balance, 221 | recvWindow=recv_window) 222 | return r.json() 223 | 224 | def get_agg_trades(self, symbol, 225 | start_time: datetime = None, 226 | end_time: datetime = None, 227 | limit=500): 228 | """ 229 | Get compressed, aggregate trades. Trades that fill at the same time, 230 | from the same order, with the same price will have the quantity 231 | aggregated. If both startTime and endTime are sent, time between 232 | startTime and endTime must be less than 1 hour. 233 | 234 | :param symbol: 235 | :param start_time: Timestamp in ms to get aggregate trades from 236 | INCLUSIVE. 237 | :param end_time: Timestamp in ms to get aggregate trades from INCLUSIVE 238 | :param limit: Default 500; max 1000. 239 | :return: dict object 240 | 241 | Response: 242 | [ 243 | { 244 | "a":1582595833, // Aggregate tradeId 245 | "p":"8980.4", // Price 246 | "q":"0.0", // Quantity (should be ignored) 247 | "T":1580204505793, // Timestamp 248 | "m":false, // Was the buyer the maker 249 | } 250 | ] 251 | """ 252 | if limit > CurrencyComConstants.AGG_TRADES_MAX_LIMIT: 253 | raise ValueError('Limit should not exceed {}'.format( 254 | CurrencyComConstants.AGG_TRADES_MAX_LIMIT 255 | )) 256 | 257 | if start_time and end_time \ 258 | and end_time - start_time > timedelta(hours=1): 259 | raise ValueError( 260 | 'If both startTime and endTime are sent,' 261 | ' time between startTime and endTime must be less than 1 hour.' 262 | ) 263 | 264 | params = {'symbol': symbol, 'limit': limit} 265 | 266 | if start_time: 267 | params['startTime'] = self._to_epoch_miliseconds(start_time) 268 | 269 | if end_time: 270 | params['endTime'] = self._to_epoch_miliseconds(end_time) 271 | 272 | r = requests.get(CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 273 | params=params) 274 | 275 | return r.json() 276 | 277 | def close_trading_position(self, position_id, recv_window=None): 278 | """ 279 | Close an active leverage trade. 280 | 281 | :param position_id: 282 | :param recv_window: The value cannot be greater than 60000. 283 | :return: dict object 284 | 285 | Response example: 286 | Example: 287 | { 288 | "request": [ 289 | { 290 | "id": 242057, 291 | "accountId": 2376109060084932, 292 | "instrumentId": "45076691096786116", 293 | "rqType": "ORDER_NEW", 294 | "state": "PROCESSED", 295 | "createdTimestamp": 1587031306969 296 | } 297 | ] 298 | } 299 | """ 300 | self._validate_recv_window(recv_window) 301 | 302 | r = self._post( 303 | CurrencyComConstants.CLOSE_TRADING_POSITION_ENDPOINT, 304 | positionId=position_id, 305 | recvWindow=recv_window 306 | ) 307 | return r.json() 308 | 309 | def get_order_book(self, symbol, limit=100): 310 | """ 311 | Order book 312 | 313 | :param symbol: 314 | :param limit: Default 100; max 1000. 315 | Valid limits:[5, 10, 20, 50, 100, 500, 1000, 5000] 316 | :return: dict object 317 | Response: 318 | { 319 | "lastUpdateId": 1027024, 320 | "asks": [ 321 | [ 322 | "4.00000200", // PRICE 323 | "12.00000000" // QTY 324 | ] 325 | ], 326 | "bids": [ 327 | [ 328 | "4.00000000", // PRICE 329 | "431.00000000" // QTY 330 | ] 331 | ] 332 | } 333 | """ 334 | self._validate_limit(limit) 335 | r = requests.get(CurrencyComConstants.ORDER_BOOK_ENDPOINT, 336 | params={'symbol': symbol, 'limit': limit}) 337 | return r.json() 338 | 339 | @staticmethod 340 | def get_exchange_info(): 341 | """ 342 | Current exchange trading rules and symbol information. 343 | 344 | :return: dict object 345 | Response: 346 | { 347 | "timezone": "UTC", 348 | "serverTime": 1577178958852, 349 | "rateLimits": [ 350 | { 351 | //These are defined in the `ENUM definitions` 352 | // section under `Rate Limiters (rateLimitType)`. 353 | //All limits are optional 354 | } 355 | ], 356 | "symbols": [ 357 | { 358 | "symbol": "DPW", 359 | "name":"Deutsche Post", 360 | "status": "TRADING", 361 | "baseAsset": "DPW", 362 | "baseAssetPrecision": 3, 363 | "quoteAsset": "EUR", 364 | "quotePrecision": 3, 365 | "orderTypes": [ 366 | "LIMIT", 367 | "MARKET" 368 | ], 369 | "icebergAllowed": false, 370 | "filters": [], 371 | "marginTradingAllowed": true, 372 | "spotTradingAllowed": true 373 | }, 374 | ] 375 | } 376 | """ 377 | r = requests.get(CurrencyComConstants.EXCHANGE_INFORMATION_ENDPOINT) 378 | return r.json() 379 | 380 | def get_klines(self, symbol, 381 | interval: CandlesticksChartInervals, 382 | start_time: datetime = None, 383 | end_time: datetime = None, 384 | limit=500): 385 | """ 386 | Kline/candlestick bars for a symbol. Klines are uniquely identified 387 | by their open time. 388 | 389 | If startTime and endTime are not sent, the most recent klines are 390 | returned. 391 | :param symbol: 392 | :param interval: 393 | :param start_time: 394 | :param end_time: 395 | :param limit:Default 500; max 1000. 396 | :return: dict object 397 | 398 | Response: 399 | [ 400 | [ 401 | 1499040000000, // Open time 402 | "0.01634790", // Open 403 | "0.80000000", // High 404 | "0.01575800", // Low 405 | "0.01577100", // Close 406 | "148976.11427815" // Volume. 407 | ] 408 | ] 409 | """ 410 | if limit > CurrencyComConstants.KLINES_MAX_LIMIT: 411 | raise ValueError('Limit should not exceed {}'.format( 412 | CurrencyComConstants.KLINES_MAX_LIMIT 413 | )) 414 | 415 | params = {'symbol': symbol, 416 | 'interval': interval.value, 417 | 'limit': limit} 418 | 419 | if start_time: 420 | params['startTime'] = self._to_epoch_miliseconds(start_time) 421 | if end_time: 422 | params['endTime'] = self._to_epoch_miliseconds(end_time) 423 | r = requests.get(CurrencyComConstants.KLINES_DATA_ENDPOINT, 424 | params=params) 425 | return r.json() 426 | 427 | def get_leverage_settings(self, symbol, recv_window=None): 428 | """ 429 | General leverage settings can be seen. 430 | 431 | :param symbol: Only leverage symbols allowed here 432 | (AAPL = AAPL_LEVERAGE) 433 | :param recv_window: 434 | :return: dict object 435 | 436 | Example: 437 | { 438 | "values": [ 439 | 2, 440 | 5, 441 | 10, 442 | 20, 443 | 50, 444 | 100 445 | ], // the possible leverage sizes; 446 | "value": 20 // depicts a default leverage size which will be set in 447 | case you don’t mention the ‘leverage’ parameter in the 448 | corresponding requests. 449 | } 450 | """ 451 | self._validate_recv_window(recv_window) 452 | 453 | r = self._get( 454 | CurrencyComConstants.LEVERAGE_SETTINGS_ENDPOINT, 455 | symbol=symbol, 456 | recvWindow=recv_window 457 | ) 458 | return r.json() 459 | 460 | def get_account_trade_list(self, symbol, 461 | start_time: datetime = None, 462 | end_time: datetime = None, 463 | limit=500, 464 | recv_window=None): 465 | """ 466 | Get trades for a specific account and symbol. 467 | 468 | :param symbol: Symbol - In order to receive orders within an ‘exchange’ 469 | trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: 470 | ‘BTC%2FUSD’. 471 | In order to mention the right symbolLeverage it should be checked with 472 | the ‘symbol’ parameter value from the exchangeInfo endpoint. In case 473 | ‘symbol’ has currencies in its name then the following format should be 474 | used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name 475 | then for the leverage trading mode the following format is correct: 476 | ‘Oil%20-%20Brent.’ 477 | :param start_time: 478 | :param end_time: 479 | :param limit: Default Value: 500; Max Value: 1000. 480 | :param recv_window: The value cannot be greater than 60000. 481 | Default value : 5000 482 | :return: dict object 483 | Response: 484 | [ 485 | { 486 | "symbol": "BTC/USD", 487 | "orderId": "100234", 488 | "orderListId": -1, 489 | "price": "4.00000100", 490 | "qty": "12.00000000", 491 | "quoteQty": "48.000012", 492 | "commission": "10.10000000", 493 | "commissionAsset": "BTC", 494 | "time": 1499865549590, 495 | "isBuyer": true, 496 | "isMaker": false 497 | } 498 | ] 499 | """ 500 | self._validate_limit(limit) 501 | self._validate_recv_window(recv_window) 502 | 503 | params = {'symbol': symbol, 'limit': limit, 'recvWindow': recv_window} 504 | 505 | if start_time: 506 | params['startTime'] = self._to_epoch_miliseconds(start_time) 507 | 508 | if end_time: 509 | params['endTime'] = self._to_epoch_miliseconds(end_time) 510 | 511 | r = self._get(CurrencyComConstants.ACCOUNT_TRADE_LIST_ENDPOINT, 512 | **params) 513 | 514 | return r.json() 515 | 516 | def get_open_orders(self, symbol=None, recv_window=None): 517 | """ 518 | Get all open orders on a symbol. Careful when accessing this with no 519 | symbol. 520 | If the symbol is not sent, orders for all symbols will be returned in 521 | an array. 522 | 523 | :param symbol: Symbol - In order to receive orders within an ‘exchange’ 524 | trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: 525 | ‘BTC%2FUSD’. 526 | In order to mention the right symbolLeverage it should be checked with 527 | the ‘symbol’ parameter value from the exchangeInfo endpoint. In case 528 | ‘symbol’ has currencies in its name then the following format should be 529 | used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name 530 | then for the leverage trading mode the following format is correct: 531 | ‘Oil%20-%20Brent.’ 532 | :param recv_window: The value cannot be greater than 60000. 533 | :return: dict object 534 | 535 | Response: 536 | [ 537 | { 538 | "symbol": "LTC/BTC", 539 | "orderId": "1", 540 | "orderListId": -1, 541 | "clientOrderId": "myOrder1", 542 | "price": "0.1", 543 | "origQty": "1.0", 544 | "executedQty": "0.0", 545 | "cummulativeQuoteQty": "0.0", 546 | "status": "NEW", 547 | "timeInForce": "GTC", 548 | "type": "LIMIT", 549 | "side": "BUY", 550 | "stopPrice": "0.0", 551 | "time": 1499827319559, 552 | "updateTime": 1499827319559, 553 | "isWorking": true, 554 | "origQuoteOrderQty": "0.000000" 555 | } 556 | ] 557 | """ 558 | 559 | self._validate_recv_window(recv_window) 560 | 561 | r = self._get(CurrencyComConstants.CURRENT_OPEN_ORDERS_ENDPOINT, 562 | symbol=symbol, 563 | recvWindow=recv_window) 564 | return r.json() 565 | 566 | def new_order(self, 567 | symbol, 568 | side: OrderSide, 569 | order_type: OrderType, 570 | quantity: float, 571 | account_id: str = None, 572 | expire_timestamp: datetime = None, 573 | guaranteed_stop_loss: bool = False, 574 | stop_loss: float = None, 575 | take_profit: float = None, 576 | leverage: int = None, 577 | price: float = None, 578 | new_order_resp_type: NewOrderResponseType 579 | = NewOrderResponseType.FULL, 580 | recv_window=None 581 | ): 582 | """ 583 | To create a market or limit order in the exchange trading mode, and 584 | market, limit or stop order in the leverage trading mode. 585 | Please note that to open an order within the ‘leverage’ trading mode 586 | symbolLeverage should be used and additional accountId parameter should 587 | be mentioned in the request. 588 | :param symbol: In order to mention the right symbolLeverage it should 589 | be checked with the ‘symbol’ parameter value from the exchangeInfo 590 | endpoint. In case ‘symbol’ has currencies in its name then the 591 | following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case 592 | ‘symbol’ has only an asset name then for the leverage trading mode the 593 | following format is correct: ‘Oil%20-%20Brent’. 594 | :param side: 595 | :param order_type: 596 | :param quantity: 597 | :param account_id: 598 | :param expire_timestamp: 599 | :param guaranteed_stop_loss: 600 | :param stop_loss: 601 | :param take_profit: 602 | :param leverage: 603 | :param price: Required for LIMIT orders 604 | :param new_order_resp_type: newOrderRespType in the exchange trading 605 | mode for MARKET order RESULT or FULL can be mentioned. MARKET order 606 | type default to FULL. LIMIT order type can be only RESULT. For the 607 | leverage trading mode only RESULT is available. 608 | :param recv_window: The value cannot be greater than 60000. 609 | :return: dict object 610 | 611 | Response RESULT: 612 | { 613 | "clientOrderId" : "00000000-0000-0000-0000-00000002cac8", 614 | "status" : "FILLED", 615 | "cummulativeQuoteQty" : null, 616 | "executedQty" : "0.001", 617 | "type" : "MARKET", 618 | "transactTime" : 1577446511069, 619 | "origQty" : "0.001", 620 | "symbol" : "BTC/USD", 621 | "timeInForce" : "FOK", 622 | "side" : "BUY", 623 | "price" : "7173.6186", 624 | "orderId" : "00000000-0000-0000-0000-00000002cac8" 625 | } 626 | Response FULL: 627 | { 628 | "orderId" : "00000000-0000-0000-0000-00000002ca43", 629 | "price" : "7183.3881", 630 | "clientOrderId" : "00000000-0000-0000-0000-00000002ca43", 631 | "side" : "BUY", 632 | "cummulativeQuoteQty" : null, 633 | "origQty" : "0.001", 634 | "transactTime" : 1577445603997, 635 | "type" : "MARKET", 636 | "executedQty" : "0.001", 637 | "status" : "FILLED", 638 | "fills" : [ 639 | { 640 | "price" : "7169.05", 641 | "qty" : "0.001", 642 | "commissionAsset" : "dUSD", 643 | "commission" : "0" 644 | } 645 | ], 646 | "timeInForce" : "FOK", 647 | "symbol" : "BTC/USD" 648 | } 649 | """ 650 | self._validate_recv_window(recv_window) 651 | self._validate_new_order_resp_type(new_order_resp_type, order_type) 652 | 653 | if order_type == OrderType.LIMIT: 654 | if not price: 655 | raise ValueError('For LIMIT orders price is required or ' 656 | f'should be greater than 0. Got {price}') 657 | 658 | expire_timestamp_epoch = self._to_epoch_miliseconds(expire_timestamp) 659 | 660 | r = self._post( 661 | CurrencyComConstants.ORDER_ENDPOINT, 662 | accountId=account_id, 663 | expireTimestamp=expire_timestamp_epoch, 664 | guaranteedStopLoss=guaranteed_stop_loss, 665 | leverage=leverage, 666 | newOrderRespType=new_order_resp_type.value, 667 | price=price, 668 | quantity=quantity, 669 | recvWindow=recv_window, 670 | side=side.value, 671 | stopLoss=stop_loss, 672 | symbol=symbol, 673 | takeProfit=take_profit, 674 | type=order_type.value, 675 | ) 676 | return r.json() 677 | 678 | def cancel_order(self, symbol, 679 | order_id, 680 | recv_window=None): 681 | """ 682 | Cancel an active order within exchange and leverage trading modes. 683 | 684 | :param symbol: 685 | :param order_id: 686 | :param recv_window: The value cannot be greater than 60000. 687 | :return: dict object 688 | 689 | Response: 690 | { 691 | "symbol": "LTC/BTC", 692 | "origClientOrderId": "myOrder1", 693 | "orderId": "4", 694 | "orderListId": -1, 695 | "clientOrderId": "cancelMyOrder1", 696 | "price": "2.00000000", 697 | "origQty": "1.00000000", 698 | "executedQty": "0.00000000", 699 | "cummulativeQuoteQty": "0.00000000", 700 | "status": "CANCELED", 701 | "timeInForce": "GTC", 702 | "type": "LIMIT", 703 | "side": "BUY" 704 | } 705 | """ 706 | 707 | self._validate_recv_window(recv_window) 708 | 709 | r = self._delete( 710 | CurrencyComConstants.ORDER_ENDPOINT, 711 | symbol=symbol, 712 | orderId=order_id, 713 | recvWindow=recv_window 714 | ) 715 | return r.json() 716 | 717 | @staticmethod 718 | def get_24h_price_change(symbol=None): 719 | """ 720 | 24-hour rolling window price change statistics. Careful when accessing 721 | this with no symbol. 722 | If the symbol is not sent, tickers for all symbols will be returned in 723 | an array. 724 | :param symbol: 725 | :return: dict object 726 | 727 | Response: 728 | { 729 | "symbol": "LTC/USD", 730 | "priceChange": "0.88", 731 | "priceChangePercent": "1.49", 732 | "weightedAvgPrice": "59.29", 733 | "prevClosePrice": "58.37", 734 | "lastPrice": "59.25", 735 | "lastQty": "220.0", 736 | "bidPrice": "59.25", 737 | "askPrice": "59.32", 738 | "openPrice": "58.37", 739 | "highPrice": "61.39", 740 | "lowPrice": "58.37", 741 | "volume": "22632", 742 | "quoteVolume": "440.0", 743 | "openTime": 1580169600000, 744 | "closeTime": 1580205307222, 745 | "firstId": 0, 746 | "lastId": 0, 747 | "count": 0 748 | } 749 | 750 | OR 751 | 752 | { 753 | "symbol": "LTC/USD", 754 | "priceChange": null, 755 | "priceChangePercent": null, 756 | "weightedAvgPrice": "59.29", 757 | "prevClosePrice": null, 758 | "lastPrice": "59.23", 759 | "lastQty": "220.0", 760 | "bidPrice": "59.23", 761 | "askPrice": "59.35", 762 | "openPrice": null, 763 | "highPrice": null, 764 | "lowPrice": null, 765 | "volume": null, 766 | "quoteVolume": "432.18", 767 | "openTime": 0, 768 | "closeTime": 0, 769 | "firstId": 0, 770 | "lastId": 0, 771 | "count": 0 772 | } 773 | """ 774 | r = requests.get(CurrencyComConstants.PRICE_CHANGE_24H_ENDPOINT, 775 | params={'symbol': symbol} if symbol else {}) 776 | return r.json() 777 | 778 | @staticmethod 779 | def get_server_time(): 780 | """ 781 | Test connectivity to the API and get the current server time. 782 | 783 | :return: dict object 784 | Response: 785 | { 786 | "serverTime": 1499827319559 787 | } 788 | """ 789 | r = requests.get(CurrencyComConstants.SERVER_TIME_ENDPOINT) 790 | 791 | return r.json() 792 | 793 | def list_leverage_trades(self, recv_window=None): 794 | """ 795 | 796 | :param recv_window:recvWindow cannot be greater than 60000 797 | Default value : 5000 798 | :return: dict object 799 | Example: 800 | { 801 | "positions": [ 802 | { 803 | "accountId": 2376109060084932, 804 | "id": "00a02503-0079-54c4-0000-00004067006b", 805 | "instrumentId": "45076691096786116", 806 | "orderId": "00a02503-0079-54c4-0000-00004067006a", 807 | "openQuantity": 0.01, 808 | "openPrice": 6734.4, 809 | "closeQuantity": 0.0, 810 | "closePrice": 0, 811 | "takeProfit": 7999.15, 812 | "stopLoss": 5999.15, 813 | "guaranteedStopLoss": false, 814 | "rpl": 0, 815 | "rplConverted": 0, 816 | "swap": -0.00335894, 817 | "swapConverted": -0.00335894, 818 | "fee": -0.050508, 819 | "dividend": 0, 820 | "margin": 0.5, 821 | "state": "ACTIVE", 822 | "currency": "USD", 823 | "createdTimestamp": 1586953061455, 824 | "openTimestamp": 1586953061243, 825 | "cost": 33.73775, 826 | "symbol": “BTC/USD_LEVERAGE” 827 | }, 828 | ..... 829 | ] 830 | } 831 | """ 832 | self._validate_recv_window(recv_window) 833 | r = self._get( 834 | CurrencyComConstants.TRADING_POSITIONS_ENDPOINT, 835 | recvWindow=recv_window 836 | ) 837 | return r.json() 838 | 839 | def update_trading_position(self, 840 | position_id, 841 | stop_loss: float = None, 842 | take_profit: float = None, 843 | guaranteed_stop_loss=False, 844 | recv_window=None): 845 | """ 846 | To edit current leverage trade by changing stop loss and take profit 847 | levels. 848 | 849 | :return: dict object 850 | Example: 851 | { 852 | "requestId": 242040, 853 | "state": “PROCESSED” 854 | } 855 | """ 856 | self._validate_recv_window(recv_window) 857 | r = self._post( 858 | CurrencyComConstants.UPDATE_TRADING_POSITION_ENDPOINT, 859 | positionId=position_id, 860 | guaranteedStopLoss=guaranteed_stop_loss, 861 | stopLoss=stop_loss, 862 | takeProfit=take_profit 863 | ) 864 | return r.json() 865 | -------------------------------------------------------------------------------- /integration_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sann05/python-currencycom/d790bd008e1cc83e64df3686f5b65ba751d9772a/integration_tests/__init__.py -------------------------------------------------------------------------------- /integration_tests/rest/README.md: -------------------------------------------------------------------------------- 1 | To run this package we need to add environment variables 2 | API_KEY: Currency.com test account api key 3 | API_SECRET: Currency.com test account api secret -------------------------------------------------------------------------------- /integration_tests/rest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sann05/python-currencycom/d790bd008e1cc83e64df3686f5b65ba751d9772a/integration_tests/rest/__init__.py -------------------------------------------------------------------------------- /integration_tests/rest/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from currencycom.client import Client 6 | 7 | API_KEY_VAR = "API_KEY" 8 | API_SECRET_VAR = "API_SECRET" 9 | 10 | 11 | @pytest.fixture(scope="session", autouse=True) 12 | def check_env_variables(): 13 | required_vars = [API_KEY_VAR, API_SECRET_VAR] 14 | missing_vars = [] 15 | for var in required_vars: 16 | if var not in os.environ: 17 | missing_vars.append(var) 18 | 19 | if len(missing_vars) > 0: 20 | raise EnvironmentError("Missing required environmental variables: " 21 | f"{', '.join(missing_vars)}") 22 | 23 | 24 | @pytest.fixture(scope='function') 25 | def client(): 26 | client = Client(os.environ[API_KEY_VAR], os.environ[API_SECRET_VAR]) 27 | return client 28 | -------------------------------------------------------------------------------- /integration_tests/rest/test_account.py: -------------------------------------------------------------------------------- 1 | class TestAccount: 2 | def test_base(self, client): 3 | account_info = client.get_account_info() 4 | assert len(account_info) > 0 5 | 6 | def test_show_balances_without_0(self, client): 7 | account_info = client.get_account_info(show_zero_balance=False) 8 | assert all((balance["free"] + balance["free"]) > 0 9 | for balance in account_info["balances"]) 10 | 11 | def test_show_balances_with_0(self, client): 12 | account_info = client.get_account_info(show_zero_balance=True) 13 | assert not all((balance["free"] + balance["free"]) > 0 14 | for balance in account_info["balances"]) 15 | -------------------------------------------------------------------------------- /integration_tests/wss/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sann05/python-currencycom/d790bd008e1cc83e64df3686f5b65ba751d9772a/integration_tests/wss/__init__.py -------------------------------------------------------------------------------- /push_new_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | +ex 4 | bash ./check_code.sh 5 | twine upload -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.0 2 | pytest==5.3.5 3 | flake8==5.0.4 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name='python-currencycom', 9 | version='0.2.2', 10 | packages=['currencycom'], 11 | description='Currency.com REST API python implementation', 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url='https://github.com/sann05/python-currencycom', 15 | author='Aliaksandr Sheliutsin', 16 | license='MIT', 17 | author_email='', 18 | install_requires=['requests', ], 19 | keywords="currencycom exchange rest wss websocket api bitcoin ethereum " 20 | "btc eth", 21 | classifiers=[ 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Programming Language :: Python :: 3.8', 30 | 'Programming Language :: Python', 31 | 'Topic :: Software Development :: Libraries :: Python Modules', 32 | ], 33 | python_requires='>=3.5', 34 | ) 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sann05/python-currencycom/d790bd008e1cc83e64df3686f5b65ba751d9772a/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope='function') 7 | def mock_requests(monkeypatch): 8 | mock = MagicMock() 9 | monkeypatch.setattr('requests.get', mock) 10 | return mock 11 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | from unittest.mock import MagicMock 3 | 4 | import pytest 5 | 6 | from currencycom.client import * 7 | 8 | 9 | class TestClient(object): 10 | @pytest.fixture(autouse=True) 11 | def set_client(self, mock_requests): 12 | self.client = Client('', '') 13 | self.mock_requests = mock_requests 14 | 15 | def test_not_called(self): 16 | self.mock_requests.assert_not_called() 17 | 18 | def test_get_server_time(self, monkeypatch): 19 | self.client.get_server_time() 20 | self.mock_requests.assert_called_once_with( 21 | CurrencyComConstants.SERVER_TIME_ENDPOINT 22 | ) 23 | 24 | def test_get_exchange_info(self): 25 | self.client.get_exchange_info() 26 | self.mock_requests.assert_called_once_with( 27 | CurrencyComConstants.EXCHANGE_INFORMATION_ENDPOINT 28 | ) 29 | 30 | def test_get_order_book_default(self, monkeypatch): 31 | val_lim_mock = MagicMock() 32 | monkeypatch.setattr(self.client, '_validate_limit', val_lim_mock) 33 | symbol = 'TEST' 34 | self.client.get_order_book(symbol) 35 | self.mock_requests.assert_called_once_with( 36 | CurrencyComConstants.ORDER_BOOK_ENDPOINT, 37 | params={'symbol': symbol, 'limit': 100} 38 | ) 39 | val_lim_mock.assert_called_once_with(100) 40 | 41 | def test_get_order_book_with_limit(self, monkeypatch): 42 | val_lim_mock = MagicMock() 43 | limit = 500 44 | monkeypatch.setattr(self.client, '_validate_limit', val_lim_mock) 45 | symbol = 'TEST' 46 | self.client.get_order_book(symbol, limit) 47 | self.mock_requests.assert_called_once_with( 48 | CurrencyComConstants.ORDER_BOOK_ENDPOINT, 49 | params={'symbol': symbol, 'limit': limit} 50 | ) 51 | val_lim_mock.assert_called_once_with(limit) 52 | 53 | def test_get_agg_trades_default(self): 54 | symbol = 'TEST' 55 | self.client.get_agg_trades(symbol) 56 | self.mock_requests.assert_called_once_with( 57 | CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 58 | params={'symbol': symbol, 'limit': 500} 59 | ) 60 | 61 | def test_get_agg_trades_limit_set(self): 62 | symbol = 'TEST' 63 | limit = 20 64 | self.client.get_agg_trades(symbol, limit=limit) 65 | self.mock_requests.assert_called_once_with( 66 | CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 67 | params={'symbol': symbol, 'limit': limit} 68 | ) 69 | 70 | def test_get_agg_trades_max_limit(self): 71 | symbol = 'TEST' 72 | limit = CurrencyComConstants.AGG_TRADES_MAX_LIMIT 73 | self.client.get_agg_trades(symbol, limit=limit) 74 | self.mock_requests.assert_called_once_with( 75 | CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 76 | params={'symbol': symbol, 'limit': limit} 77 | ) 78 | 79 | def test_get_agg_trades_exceed_limit(self): 80 | symbol = 'TEST' 81 | limit = CurrencyComConstants.AGG_TRADES_MAX_LIMIT + 1 82 | with pytest.raises(ValueError): 83 | self.client.get_agg_trades(symbol, limit=limit) 84 | self.mock_requests.assert_not_called() 85 | 86 | def test_get_agg_trades_only_start_time_set(self): 87 | symbol = 'TEST' 88 | start_time = datetime(2019, 1, 1, 1, 1, 1) 89 | self.client.get_agg_trades(symbol, start_time=start_time) 90 | self.mock_requests.assert_called_once_with( 91 | CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 92 | params={'symbol': symbol, 'limit': 500, 93 | 'startTime': start_time.timestamp() * 1000} 94 | ) 95 | 96 | def test_get_agg_trades_only_end_time_set(self): 97 | symbol = 'TEST' 98 | end_time = datetime(2019, 1, 1, 1, 1, 1) 99 | self.client.get_agg_trades(symbol, end_time=end_time) 100 | self.mock_requests.assert_called_once_with( 101 | CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 102 | params={'symbol': symbol, 'limit': 500, 103 | 'endTime': end_time.timestamp() * 1000} 104 | ) 105 | 106 | def test_get_agg_trades_both_time_set(self): 107 | symbol = 'TEST' 108 | start_time = datetime(2019, 1, 1, 1, 1, 1) 109 | end_time = datetime(2019, 1, 1, 1, 1, 20) 110 | self.client.get_agg_trades(symbol, 111 | start_time=start_time, 112 | end_time=end_time) 113 | self.mock_requests.assert_called_once_with( 114 | CurrencyComConstants.AGGREGATE_TRADE_LIST_ENDPOINT, 115 | params={'symbol': symbol, 'limit': 500, 116 | 'startTime': start_time.timestamp() * 1000, 117 | 'endTime': end_time.timestamp() * 1000} 118 | ) 119 | 120 | def test_get_agg_trades_both_time_set_exceed_max_range(self): 121 | symbol = 'TEST' 122 | start_time = datetime(2019, 1, 1, 1, 1, 1) 123 | end_time = datetime(2019, 1, 1, 2, 2, 20) 124 | with pytest.raises(ValueError): 125 | self.client.get_agg_trades(symbol, 126 | start_time=start_time, 127 | end_time=end_time) 128 | self.mock_requests.assert_not_called() 129 | 130 | def test_get_klines_default(self): 131 | symbol = 'TEST' 132 | self.client.get_klines(symbol, CandlesticksChartInervals.DAY) 133 | self.mock_requests.assert_called_once_with( 134 | CurrencyComConstants.KLINES_DATA_ENDPOINT, 135 | params={'symbol': symbol, 136 | 'interval': CandlesticksChartInervals.DAY.value, 137 | 'limit': 500} 138 | ) 139 | 140 | def test_get_klines_with_limit(self): 141 | symbol = 'TEST' 142 | limit = 123 143 | self.client.get_klines(symbol, CandlesticksChartInervals.DAY, 144 | limit=limit) 145 | self.mock_requests.assert_called_once_with( 146 | CurrencyComConstants.KLINES_DATA_ENDPOINT, 147 | params={'symbol': symbol, 148 | 'interval': CandlesticksChartInervals.DAY.value, 149 | 'limit': limit} 150 | ) 151 | 152 | def test_get_klines_max_limit(self): 153 | symbol = 'TEST' 154 | limit = CurrencyComConstants.KLINES_MAX_LIMIT 155 | self.client.get_klines(symbol, CandlesticksChartInervals.DAY, 156 | limit=limit) 157 | self.mock_requests.assert_called_once_with( 158 | CurrencyComConstants.KLINES_DATA_ENDPOINT, 159 | params={'symbol': symbol, 160 | 'interval': CandlesticksChartInervals.DAY.value, 161 | 'limit': limit} 162 | ) 163 | 164 | def test_get_klines_exceed_max_limit(self): 165 | symbol = 'TEST' 166 | limit = CurrencyComConstants.KLINES_MAX_LIMIT + 1 167 | with pytest.raises(ValueError): 168 | self.client.get_klines(symbol, CandlesticksChartInervals.DAY, 169 | limit=limit) 170 | self.mock_requests.assert_not_called() 171 | 172 | def test_get_klines_with_startTime(self): 173 | symbol = 'TEST' 174 | start_date = datetime(2020, 1, 1) 175 | self.client.get_klines(symbol, 176 | CandlesticksChartInervals.DAY, 177 | start_time=start_date) 178 | self.mock_requests.assert_called_once_with( 179 | CurrencyComConstants.KLINES_DATA_ENDPOINT, 180 | params={'symbol': symbol, 181 | 'interval': CandlesticksChartInervals.DAY.value, 182 | 'startTime': int(start_date.timestamp() * 1000), 183 | 'limit': 500} 184 | ) 185 | 186 | def test_get_klines_with_endTime(self): 187 | symbol = 'TEST' 188 | end_time = datetime(2020, 1, 1) 189 | self.client.get_klines(symbol, 190 | CandlesticksChartInervals.DAY, 191 | end_time=end_time) 192 | self.mock_requests.assert_called_once_with( 193 | CurrencyComConstants.KLINES_DATA_ENDPOINT, 194 | params={'symbol': symbol, 195 | 'interval': CandlesticksChartInervals.DAY.value, 196 | 'endTime': int(end_time.timestamp() * 1000), 197 | 'limit': 500} 198 | ) 199 | 200 | def test_get_klines_with_startTime_and_endTime(self): 201 | symbol = 'TEST' 202 | start_time = datetime(2020, 1, 1) 203 | end_time = datetime(2021, 1, 1) 204 | self.client.get_klines(symbol, 205 | CandlesticksChartInervals.DAY, 206 | start_time=start_time, 207 | end_time=end_time) 208 | self.mock_requests.assert_called_once_with( 209 | CurrencyComConstants.KLINES_DATA_ENDPOINT, 210 | params={'symbol': symbol, 211 | 'interval': CandlesticksChartInervals.DAY.value, 212 | 'startTime': int(start_time.timestamp() * 1000), 213 | 'endTime': int(end_time.timestamp() * 1000), 214 | 'limit': 500} 215 | ) 216 | 217 | def test_get_24h_price_change_default(self): 218 | self.client.get_24h_price_change() 219 | self.mock_requests.assert_called_once_with( 220 | CurrencyComConstants.PRICE_CHANGE_24H_ENDPOINT, 221 | params={} 222 | ) 223 | 224 | def test_get_24h_price_change_with_symbol(self): 225 | symbol = 'TEST' 226 | self.client.get_24h_price_change(symbol) 227 | self.mock_requests.assert_called_once_with( 228 | CurrencyComConstants.PRICE_CHANGE_24H_ENDPOINT, 229 | params={'symbol': symbol} 230 | ) 231 | 232 | def test_new_order_default_buy(self, monkeypatch): 233 | post_mock = MagicMock() 234 | monkeypatch.setattr(self.client, '_post', post_mock) 235 | symbol = 'TEST' 236 | side = OrderSide.BUY 237 | ord_type = OrderType.MARKET 238 | amount = 1 239 | self.client.new_order(symbol, side, ord_type, amount) 240 | post_mock.assert_called_once_with( 241 | CurrencyComConstants.ORDER_ENDPOINT, 242 | accountId=None, 243 | expireTimestamp=None, 244 | guaranteedStopLoss=False, 245 | newOrderRespType=ANY, 246 | price=ANY, 247 | quantity=amount, 248 | recvWindow=ANY, 249 | side=side.value, 250 | stopLoss=None, 251 | symbol=symbol, 252 | takeProfit=None, 253 | leverage=None, 254 | type=ord_type.value, 255 | ) 256 | 257 | def test_new_order_default_sell(self, monkeypatch): 258 | post_mock = MagicMock() 259 | monkeypatch.setattr(self.client, '_post', post_mock) 260 | symbol = 'TEST' 261 | side = OrderSide.BUY 262 | ord_type = OrderType.MARKET 263 | amount = 1 264 | self.client.new_order(symbol, side, ord_type, amount) 265 | post_mock.assert_called_once_with( 266 | CurrencyComConstants.ORDER_ENDPOINT, 267 | accountId=None, 268 | expireTimestamp=None, 269 | guaranteedStopLoss=False, 270 | newOrderRespType=ANY, 271 | price=ANY, 272 | quantity=amount, 273 | recvWindow=ANY, 274 | side=side.value, 275 | stopLoss=None, 276 | symbol=symbol, 277 | takeProfit=None, 278 | leverage=None, 279 | type=ord_type.value, 280 | ) 281 | 282 | def test_new_order_invalid_recv_window(self, monkeypatch): 283 | symbol = 'TEST' 284 | side = OrderSide.BUY 285 | ord_type = OrderType.MARKET 286 | amount = 1 287 | with pytest.raises(ValueError): 288 | self.client.new_order( 289 | symbol, side, ord_type, amount, 290 | recv_window=CurrencyComConstants.RECV_WINDOW_MAX_LIMIT + 1) 291 | self.mock_requests.assert_not_called() 292 | 293 | def test_new_order_default_limit(self, monkeypatch): 294 | post_mock = MagicMock() 295 | monkeypatch.setattr(self.client, '_post', post_mock) 296 | symbol = 'TEST' 297 | side = OrderSide.BUY 298 | ord_type = OrderType.LIMIT 299 | new_order_resp_type = NewOrderResponseType.RESULT 300 | amount = 1 301 | price = 1 302 | self.client.new_order(symbol, 303 | side, 304 | ord_type, 305 | price=price, 306 | new_order_resp_type=new_order_resp_type, 307 | quantity=amount) 308 | post_mock.assert_called_once_with( 309 | CurrencyComConstants.ORDER_ENDPOINT, 310 | accountId=None, 311 | expireTimestamp=None, 312 | guaranteedStopLoss=False, 313 | newOrderRespType=new_order_resp_type.value, 314 | price=ANY, 315 | quantity=amount, 316 | recvWindow=ANY, 317 | side=side.value, 318 | stopLoss=None, 319 | symbol=symbol, 320 | takeProfit=None, 321 | leverage=None, 322 | type=ord_type.value, 323 | ) 324 | 325 | def test_new_order_incorrect_limit_no_price(self, monkeypatch): 326 | post_mock = MagicMock() 327 | monkeypatch.setattr(self.client, '_post', post_mock) 328 | symbol = 'TEST' 329 | side = OrderSide.BUY 330 | ord_type = OrderType.LIMIT 331 | amount = 1 332 | with pytest.raises(ValueError): 333 | self.client.new_order(symbol, 334 | side, 335 | ord_type, 336 | quantity=amount) 337 | post_mock.assert_not_called() 338 | 339 | def test_new_order_incorrect_limit_no_time_in_force(self, monkeypatch): 340 | post_mock = MagicMock() 341 | monkeypatch.setattr(self.client, '_post', post_mock) 342 | symbol = 'TEST' 343 | side = OrderSide.BUY 344 | ord_type = OrderType.LIMIT 345 | amount = 1 346 | price = 1 347 | with pytest.raises(ValueError): 348 | self.client.new_order(symbol, 349 | side, 350 | ord_type, 351 | price=price, 352 | quantity=amount) 353 | post_mock.assert_not_called() 354 | 355 | def test_cancel_order_default_order_id(self, monkeypatch): 356 | delete_mock = MagicMock() 357 | monkeypatch.setattr(self.client, '_delete', delete_mock) 358 | symbol = 'TEST' 359 | order_id = 'TEST_ORDER_ID' 360 | self.client.cancel_order(symbol, order_id) 361 | delete_mock.assert_called_once_with( 362 | CurrencyComConstants.ORDER_ENDPOINT, 363 | symbol=symbol, 364 | orderId=order_id, 365 | recvWindow=None 366 | ) 367 | 368 | def test_cancel_order_default_client_order_id(self, monkeypatch): 369 | delete_mock = MagicMock() 370 | monkeypatch.setattr(self.client, '_delete', delete_mock) 371 | symbol = 'TEST' 372 | order_id = 'TEST_ORDER_ID' 373 | self.client.cancel_order(symbol, order_id=order_id) 374 | delete_mock.assert_called_once_with( 375 | CurrencyComConstants.ORDER_ENDPOINT, 376 | symbol=symbol, 377 | orderId=order_id, 378 | recvWindow=None 379 | ) 380 | 381 | @pytest.mark.skip("order_id param became mandatory") 382 | def test_cancel_order_default_no_id(self, monkeypatch): 383 | delete_mock = MagicMock() 384 | monkeypatch.setattr(self.client, '_delete', delete_mock) 385 | symbol = 'TEST' 386 | with pytest.raises(ValueError): 387 | self.client.cancel_order(symbol) 388 | delete_mock.assert_not_called() 389 | 390 | def test_cancel_order_invalid_recv_window(self, monkeypatch): 391 | delete_mock = MagicMock() 392 | monkeypatch.setattr(self.client, '_delete', delete_mock) 393 | symbol = 'TEST' 394 | with pytest.raises(ValueError): 395 | self.client.cancel_order( 396 | symbol, 'id', 397 | recv_window=CurrencyComConstants.RECV_WINDOW_MAX_LIMIT + 1) 398 | delete_mock.assert_not_called() 399 | 400 | def test_get_open_orders_default(self, monkeypatch): 401 | get_mock = MagicMock() 402 | monkeypatch.setattr(self.client, '_get', get_mock) 403 | self.client.get_open_orders() 404 | get_mock.assert_called_once_with( 405 | CurrencyComConstants.CURRENT_OPEN_ORDERS_ENDPOINT, 406 | symbol=None, 407 | recvWindow=None 408 | ) 409 | 410 | def test_get_open_orders_with_symbol(self, monkeypatch): 411 | get_mock = MagicMock() 412 | symbol = 'Test' 413 | monkeypatch.setattr(self.client, '_get', get_mock) 414 | self.client.get_open_orders(symbol) 415 | get_mock.assert_called_once_with( 416 | CurrencyComConstants.CURRENT_OPEN_ORDERS_ENDPOINT, 417 | symbol=symbol, 418 | recvWindow=None 419 | ) 420 | 421 | def test_get_open_orders_invalid_recv_window(self): 422 | with pytest.raises(ValueError): 423 | self.client.get_open_orders( 424 | recv_window=CurrencyComConstants.RECV_WINDOW_MAX_LIMIT + 1) 425 | self.mock_requests.assert_not_called() 426 | 427 | def test_get_account_info_default(self, monkeypatch): 428 | get_mock = MagicMock() 429 | monkeypatch.setattr(self.client, '_get', get_mock) 430 | self.client.get_account_info() 431 | get_mock.assert_called_once_with( 432 | CurrencyComConstants.ACCOUNT_INFORMATION_ENDPOINT, 433 | showZeroBalance=False, 434 | recvWindow=None 435 | ) 436 | 437 | def test_get_account_info_invalid_recv_window(self): 438 | with pytest.raises(ValueError): 439 | self.client.get_account_info( 440 | recv_window=CurrencyComConstants.RECV_WINDOW_MAX_LIMIT + 1) 441 | self.mock_requests.assert_not_called() 442 | 443 | def test_get_account_trade_list_default(self, monkeypatch): 444 | get_mock = MagicMock() 445 | symbol = 'TEST' 446 | monkeypatch.setattr(self.client, '_get', get_mock) 447 | self.client.get_account_trade_list(symbol) 448 | get_mock.assert_called_once_with( 449 | CurrencyComConstants.ACCOUNT_TRADE_LIST_ENDPOINT, 450 | symbol=symbol, 451 | limit=500, 452 | recvWindow=None 453 | ) 454 | 455 | def test_get_account_trade_list_with_start_time(self, monkeypatch): 456 | get_mock = MagicMock() 457 | symbol = 'TEST' 458 | start_time = datetime(2020, 1, 1, 1, 1, 1) 459 | monkeypatch.setattr(self.client, '_get', get_mock) 460 | self.client.get_account_trade_list(symbol, start_time=start_time) 461 | get_mock.assert_called_once_with( 462 | CurrencyComConstants.ACCOUNT_TRADE_LIST_ENDPOINT, 463 | symbol=symbol, 464 | limit=500, 465 | recvWindow=None, 466 | startTime=start_time.timestamp() * 1000 467 | ) 468 | 469 | def test_get_account_trade_list_with_end_time(self, monkeypatch): 470 | get_mock = MagicMock() 471 | symbol = 'TEST' 472 | end_time = datetime(2020, 1, 1, 1, 1, 1) 473 | monkeypatch.setattr(self.client, '_get', get_mock) 474 | self.client.get_account_trade_list(symbol, end_time=end_time) 475 | get_mock.assert_called_once_with( 476 | CurrencyComConstants.ACCOUNT_TRADE_LIST_ENDPOINT, 477 | symbol=symbol, 478 | limit=500, 479 | recvWindow=None, 480 | endTime=end_time.timestamp() * 1000 481 | ) 482 | 483 | def test_get_account_trade_list_with_start_and_end_times(self, monkeypatch): 484 | get_mock = MagicMock() 485 | symbol = 'TEST' 486 | start_time = datetime(2019, 1, 1, 1, 1, 1) 487 | end_time = datetime(2020, 1, 1, 1, 1, 1) 488 | monkeypatch.setattr(self.client, '_get', get_mock) 489 | self.client.get_account_trade_list(symbol, 490 | start_time=start_time, 491 | end_time=end_time) 492 | get_mock.assert_called_once_with( 493 | CurrencyComConstants.ACCOUNT_TRADE_LIST_ENDPOINT, 494 | symbol=symbol, 495 | limit=500, 496 | recvWindow=None, 497 | startTime=start_time.timestamp() * 1000, 498 | endTime=end_time.timestamp() * 1000 499 | ) 500 | 501 | def test_get_account_trade_list_incorrect_recv_window(self): 502 | with pytest.raises(ValueError): 503 | self.client.get_account_trade_list( 504 | 'TEST', 505 | recv_window=CurrencyComConstants.RECV_WINDOW_MAX_LIMIT + 1) 506 | self.mock_requests.assert_not_called() 507 | 508 | def test_get_account_trade_list_incorrect_limit(self): 509 | with pytest.raises(ValueError): 510 | self.client.get_account_trade_list( 511 | 'TEST', 512 | limit=999) 513 | self.mock_requests.assert_not_called() 514 | 515 | def test__to_epoch_miliseconds_default(self): 516 | dttm = datetime(1999, 1, 1, 1, 1, 1) 517 | assert self.client._to_epoch_miliseconds(dttm) \ 518 | == int(dttm.timestamp() * 1000) 519 | --------------------------------------------------------------------------------