├── LICENSE ├── README.rst ├── examples ├── rest_example.py └── websocket_example.py ├── latoken ├── __init__.py ├── client.py ├── enums.py └── helpers.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LATOKEN and Nikita Oleinik 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.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | Welcome to latoken-api-v2-python-client 3 | ======================================= 4 | 5 | If you find any bugs or want to contribute, feel free to submit your improvements. 6 | 7 | Source code 8 | https://github.com/LATOKEN/latoken-api-v2-python-client 9 | 10 | REST API Documentation 11 | https://api.latoken.com/doc/v2/ 12 | 13 | STOMP Websockets Documentation 14 | https://api.latoken.com/doc/ws/ 15 | 16 | PyPI location 17 | https://pypi.org/project/latoken-api-v2-python-client/ 18 | 19 | This library covers 20 | ------------------- 21 | 22 | - Authentication of private requests for both REST API and STOMP Websockets 23 | - Asyncio websockets with the option to subscribe to multiple streams simultaneously 24 | - General market data such as historic and current prices, orderbooks, active currencies and pairs 25 | - User account balances access 26 | - Deposit address generation 27 | - Withdrawals 28 | - Transfers within the account 29 | - Transfers between accounts (to other users) 30 | - Crypto Spot Trading 31 | 32 | This library doesn't cover 33 | -------------------------- 34 | 35 | - Futures Trading 36 | - Stocks Trading 37 | - IEO Purchases 38 | - Responce exceptions are exchange generated and are not handled by the library 39 | - Logging is not implemented 40 | 41 | Quick Start 42 | ----------- 43 | 44 | Register an account on `LATOKEN `_. 45 | 46 | Generate an API key `in your account `_ with relevant permissions. 47 | 48 | There are 4 levels of API key permissions at LATOKEN: 49 | 50 | - Read only (you can view market and account data) 51 | - Trade on Spot (in addition: place and cancel orders) 52 | - Trade and Transfer (in addition: transfer within your account) 53 | - Full access (in addition: transfer to other users, deposit and withdraw) 54 | 55 | Install latoken-api-v2-python-client library: 56 | 57 | .. code-block:: bash 58 | 59 | pip install latoken-api-v2-python-client 60 | 61 | 62 | Examples of code usage: 63 | ----------------------- 64 | 65 | - `REST API `_ 66 | - `Websockets `_ 67 | -------------------------------------------------------------------------------- /examples/rest_example.py: -------------------------------------------------------------------------------- 1 | from latoken.client import LatokenClient 2 | 3 | 4 | # There are 2 simple steps: initialisation, getting data. 5 | 6 | # Firstly, we create a client object. 7 | latoken = LatokenClient() 8 | # OR (if you want to use private endpoints, you will need to provide apiKey and apiSecret arguments) 9 | # latoken = LatokenClient(apiKey = apiKey, apiSecret = apiSecret) 10 | 11 | 12 | # Secondly, we get information from LATOKEN. 13 | # Checking server time 14 | time = latoken.getServerTime() 15 | print(time) 16 | 17 | # Get all currencies and create a dictionary with tag: id pairs. 18 | # A lot of requests would require you submitting a currency id instead of the ticker (tag). 19 | currencies = latoken.getCurrencies() 20 | 21 | currencies_dict = dict() 22 | 23 | for i in range(len(currencies)): 24 | if currencies[i]['status'] == 'CURRENCY_STATUS_ACTIVE': 25 | currencies_dict[currencies[i]['tag']] = currencies[i]['id'] 26 | 27 | print(currencies_dict['BTC']) 28 | 29 | 30 | # You can combine websockets and rest api. They are implemented as one class. 31 | -------------------------------------------------------------------------------- /examples/websocket_example.py: -------------------------------------------------------------------------------- 1 | from latoken.client import LatokenClient 2 | from sortedcontainers import SortedDict 3 | import json 4 | 5 | 6 | # There are 4 simple steps: initialisation, subscribing, dealing with data, running the script. 7 | 8 | # Firstly, we create a client object. 9 | latoken = LatokenClient() 10 | # OR (if you want to use private endpoints, you will need to provide apiKey and apiSecret arguments) 11 | # latoken = LatokenClient(apiKey = apiKey, apiSecret = apiSecret) 12 | 13 | 14 | # Secondly, we subscribe to streams we want. 15 | # Let's say we want an orderbook of LA/USDT pair and tickers of LA/USDT and LA/ETH pairs in this example. 16 | # Note that you need to subscribe to different streams separately as in this example. Chaining doesn't work here. 17 | latoken.streamBook(pairs = [ 18 | '707ccdf1-af98-4e09-95fc-e685ed0ae4c6/0c3a106d-bde3-4c13-a26e-3fd2394529e5' 19 | ]) 20 | latoken.streamPairTickers(pairs = [ 21 | '707ccdf1-af98-4e09-95fc-e685ed0ae4c6/0c3a106d-bde3-4c13-a26e-3fd2394529e5', 22 | '707ccdf1-af98-4e09-95fc-e685ed0ae4c6/620f2019-33c0-423b-8a9d-cde4d7f8ef7f' 23 | ]) 24 | 25 | 26 | # Thirdly, we write a function that contains what we want to do with the received data. 27 | # This function must me async! 28 | async def consumer(message): 29 | # Create orderbook template 30 | order_book = {"bid": SortedDict(), "ask": SortedDict()} 31 | 32 | # Creating a function that would construct and undate an ordered orderbook 33 | def updateOrderbook(order_book: dict, event: dict) -> dict: 34 | """Updates orderbook with new data 35 | """ 36 | 37 | for side in ("ask", "bid"): 38 | for entry in event[side]: 39 | price = float(entry["price"]) 40 | quantity_change = float(entry["quantityChange"]) 41 | order_book[side].setdefault(price, 0) 42 | order_book[side][price] += float(quantity_change) 43 | 44 | return order_book 45 | 46 | # Insert received data into our orderbook object. 47 | # Each topic that we subscribe to is assigned a number in the order of subscription starting from 0. 48 | # 'body' part of the message returns string, so we need to load it as json 49 | if len(message['headers']) != 0: 50 | if message['headers']['subscription'] == '0': # We subscribed to LA/USDT orderbook first, so subscription is 0. 51 | order_book = updateOrderbook(order_book, json.loads(message['body'])['payload']) 52 | print(f'LA/USDT orderbook is: {order_book}') 53 | 54 | # Let's imagine we want to know 24 hours change and last price of LA/USDT and LA/ETH pairs. 55 | if message['headers']['subscription'] == '1': 56 | la_usdt_24h_change = json.loads(message['body'])['payload']['change24h'] 57 | la_usdt_last_price = json.loads(message['body'])['payload']['lastPrice'] 58 | print(f'LA/USDT last price was: {la_usdt_last_price}') 59 | print(f'LA/USDT 24 hours change was: {la_usdt_24h_change}%') 60 | 61 | if message['headers']['subscription'] == '2': 62 | la_eth_24h_change = json.loads(message['body'])['payload']['change24h'] 63 | la_eth_last_price = json.loads(message['body'])['payload']['lastPrice'] 64 | print(f'LA/ETH last price was: {la_eth_last_price}') 65 | print(f'LA/ETH 24 hours change was: {la_eth_24h_change}%') 66 | 67 | 68 | # Finally, we launch the connection and run the code. 69 | # Don't forget to put the async function as on_message argument. 70 | latoken.run(latoken.connect(on_message = consumer)) 71 | # OR (if you want to use private endpoints, you will need to set signed = True) 72 | # latoken.run(latoken.connect(signed = True, on_message = consumer)) 73 | 74 | -------------------------------------------------------------------------------- /latoken/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /latoken/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import hmac 4 | from time import time 5 | from typing import Optional 6 | 7 | import stomper 8 | import websocket 9 | import requests 10 | 11 | 12 | class LatokenClient: 13 | 14 | baseWS = 'wss://api.latoken.com/stomp' 15 | baseAPI = 'https://api.latoken.com' 16 | 17 | # REST (calls) 18 | # Basic info 19 | user_info_call = '/v2/auth/user' # PRIVATE 20 | time_call = '/v2/time' # PUBLIC 21 | # Balances (all PRIVATE) 22 | account_balances_call = '/v2/auth/account' 23 | currency_balance_by_type_call = '/v2/auth/account/currency/{currency}/{accountType}' 24 | # Orders (all PRIVATE) 25 | order_place_call = '/v2/auth/order/place' 26 | order_cancel_all_call = '/v2/auth/order/cancelAll' 27 | order_cancel_id_call = '/v2/auth/order/cancel' 28 | order_cancel_pair_call = '/v2/auth/order/cancelAll/{currency}/{quote}' 29 | order_status_call = '/v2/auth/order/getOrder/{}' # Get order by id 30 | order_pair_active_call = '/v2/auth/order/pair/{currency}/{quote}/active' 31 | order_pair_all_call = '/v2/auth/order/pair/{currency}/{quote}' 32 | order_all_call = '/v2/auth/order' # Provides orders history (closed, cancelled, placed orders) 33 | # Fees 34 | fee_levels_call = '/v2/trade/feeLevels' # PUBLIC 35 | fee_scheme_per_pair_call = '/v2/trade/fee/{currency}/{quote}' # PUBLIC 36 | fee_scheme_par_pair_and_user_call = '/v2/auth/trade/fee/{currency}/{quote}' # PRIVATE 37 | # Trades 38 | trades_user_call = '/v2/auth/trade' # PRIVATE 39 | trades_user_pair_call = '/v2/auth/trade/pair/{currency}/{quote}' # PRIVATE 40 | trades_all_call = '/v2/trade/history/{currency}/{quote}' # PUBLIC 41 | # Books (all PUBLIC) 42 | orderbook_call = '/v2/book/{currency}/{quote}' 43 | # Tickers (all PUBLIC) 44 | tickers_call = '/v2/ticker' 45 | tickers_per_pair_call = '/v2/ticker/{currency}/{quote}' 46 | # Currencies and pairs (all PUBLIC) 47 | active_currency_call = '/v2/currency' # Available path param not implemented as it returns the same as this endpoint 48 | currency_call = '/v2/currency/{currency}' 49 | quote_currency_call = '/v2/currency/quotes' 50 | active_pairs_call = '/v2/pair' # Available path param not implemented as it returns the same as this endpoint 51 | # Historic prices (all PUBLIC) 52 | weekly_chart_call = '/v2/chart/week' 53 | weekly_chart_by_pair_call = '/v2/chart/week/{currency}/{quote}' 54 | candles_call = '/v2/tradingview/history?symbol={currency}%2F{quote}&resolution={resolution}&from={from}&to={to}' 55 | # Spot transfers (all Private) 56 | deposit_spot_call = '/v2/auth/spot/deposit' 57 | withdraw_spot_call = '/v2/auth/spot/withdraw' 58 | # Transfers (all Private) 59 | transfer_by_id_call = '/v2/auth/transfer/id' 60 | transfer_by_phone_call = '/v2/auth/transfer/phone' 61 | transfer_by_email_call = '/v2/auth/transfer/email' 62 | transfer_get_all_call = '/v2/auth/transfer' 63 | # Bindings data (for deposits and withdrawals) 64 | bindings_active_call = '/v2/transaction/bindings' # PUBLIC 65 | bindings_active_currencies_call = '/v2/auth/transaction/bindings' # PRIVATE 66 | bindings_currency_call = '/v2/auth/transaction/bindings/{currency}' # PRIVATE 67 | # Transactions (all Private) 68 | deposit_address_call = '/v2/auth/transaction/depositAddress' 69 | withdrawal_request_call = '/v2/auth/transaction/withdraw' 70 | withdrawal_cancel_call = '/v2/auth/transaction/withdraw/cancel' 71 | withdrawal_confirmation_call = '/v2/auth/transaction/withdraw/confirm' 72 | withdrawal_code_resend_call = '/v2/auth/transaction/withdraw/resendCode' 73 | transaction_all_call = '/v2/auth/transaction' 74 | transaction_by_id_call = '/v2/auth/transaction/{id}' 75 | 76 | # WS (streams) 77 | # Public 78 | book_stream = '/v1/book/{currency}/{quote}' 79 | trades_stream = '/v1/trade/{currency}/{quote}' 80 | currencies_stream = '/v1/currency' # All available currencies 81 | pairs_stream = '/v1/pair' # All available pairs 82 | ticker_all_stream = '/v1/ticker' 83 | tickers_pair_stream = '/v1/ticker/{currency}/{quote}' # 24h and 7d volume and change + last price for pairs 84 | rates_stream = '/v1/rate/{currency}/{quote}' 85 | rates_quote_stream = '/v1/rate/{quote}' 86 | 87 | # Private 88 | orders_stream = '/user/{user}/v1/order' 89 | accounts_stream = '/user/{user}/v1/account/total' # Returns all accounts of a user including empty ones 90 | account_stream = '/user/{user}/v1/account' 91 | transactions_stream = '/user/{user}/v1/transaction' # Returns external transactions (deposits and withdrawals) 92 | transfers_stream = '/user/{user}/v1/transfers' # Returns internal transfers on the platform (inter_user, ...) 93 | 94 | topics = list() 95 | 96 | # INITIALISATION 97 | 98 | def __init__(self, apiKey: Optional[str] = None, apiSecret: Optional[str] = None, 99 | baseAPI: str = baseAPI, baseWS: str = baseWS, topics: list = topics): 100 | self.apiKey = apiKey 101 | self.apiSecret = apiSecret 102 | self.baseAPI = baseAPI 103 | self.baseWS = baseWS 104 | self.topics = topics 105 | 106 | # CONTROLLERS 107 | 108 | def _inputController(self, currency: Optional[str] = None, quote: Optional[str] = None, 109 | pair: Optional[str] = None, currency_name: Optional[str] = 'currency', 110 | quote_name: Optional[str] = 'quote') -> dict: 111 | """Converting lower case currency tag into upper case as required""" 112 | 113 | def controller(arg): 114 | if len(arg) == 36: 115 | return arg 116 | else: 117 | return arg.upper() 118 | 119 | if pair: 120 | currency = pair.split('/')[0] 121 | quote = pair.split('/')[1] 122 | pathParams = { 123 | str(currency_name): controller(currency), 124 | str(quote_name): controller(quote) 125 | } 126 | return pathParams 127 | 128 | elif currency and quote: 129 | pathParams = { 130 | str(currency_name): controller(currency), 131 | str(quote_name): controller(quote) 132 | } 133 | return pathParams 134 | 135 | elif currency: 136 | pathParams = { 137 | str(currency_name): controller(currency) 138 | } 139 | return pathParams 140 | 141 | # SIGNATURES 142 | 143 | def _APIsigned(self, endpoint: str, params: dict = None, request_type: Optional[str] = 'get'): 144 | """Signing get and post private calls by api key and secret by HMAC-SHA512""" 145 | 146 | if params: 147 | serializeFunc = map(lambda it: it[0] + '=' + str(it[1]), params.items()) 148 | queryParams = '&'.join(serializeFunc) 149 | else: 150 | queryParams = '' 151 | 152 | if request_type == 'get': 153 | signature = hmac.new( 154 | self.apiSecret, 155 | ('GET' + endpoint + queryParams).encode('ascii'), 156 | hashlib.sha512 157 | ) 158 | 159 | url = self.baseAPI + endpoint + '?' + queryParams 160 | 161 | response = requests.get( 162 | url, 163 | headers = { 164 | 'X-LA-APIKEY': self.apiKey, 165 | 'X-LA-SIGNATURE': signature.hexdigest(), 166 | 'X-LA-DIGEST': 'HMAC-SHA512' 167 | } 168 | ) 169 | 170 | elif request_type == 'post': 171 | signature = hmac.new( 172 | self.apiSecret, 173 | ('POST' + endpoint + queryParams).encode('ascii'), 174 | hashlib.sha512 175 | ) 176 | 177 | url = self.baseAPI + endpoint 178 | 179 | response = requests.post( 180 | url, 181 | headers = { 182 | 'Content-Type': 'application/json', 183 | 'X-LA-APIKEY': self.apiKey, 184 | 'X-LA-SIGNATURE': signature.hexdigest(), 185 | 'X-LA-DIGEST': 'HMAC-SHA512' 186 | }, 187 | json = params 188 | ) 189 | 190 | return response.json() 191 | 192 | # EXCHANGE ENDPOINTS 193 | 194 | def getUserInfo(self) -> dict: 195 | """Returns information about the authenticated user 196 | 197 | :returns: dict - dict of personal data 198 | 199 | .. code-block:: python 200 | 201 | { 202 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', # User id (unique for each user) 203 | 'status': 'ACTIVE', # Account status (ACTIVE, DISABLED, FROZEN) 204 | 'role': 'INVESTOR', # Can be ignored 205 | 'email': 'example@email.com', # Email address on user account 206 | 'phone': '', # Phone number on user account 207 | 'authorities': [..., 'VIEW_TRANSACTIONS', 'PLACE_ORDER', ...], # List of account priviliges 208 | 'forceChangePassword': None, # Can be ignored 209 | 'authType': 'API_KEY', # Can be ignored 210 | 'socials': [] # Can be ignored 211 | } 212 | 213 | """ 214 | 215 | return self._APIsigned(endpoint = self.user_info_call) 216 | 217 | 218 | def getServerTime(self) -> dict: 219 | """Returns the currenct server time 220 | 221 | :returns: dict 222 | 223 | .. code-block:: python 224 | 225 | { 226 | 'serverTime': 1628934753710 227 | } 228 | 229 | """ 230 | 231 | return requests.get(self.baseAPI + self.time_call).json() 232 | 233 | 234 | def getAccountBalances(self, currency: Optional[str] = None, account_type: Optional[str] = None, 235 | zeros: Optional[bool] = False): 236 | """Returns account balances for all/specific currency and wallet type 237 | 238 | A request for a specific currency and wallet type has a priority over all-currencies request 239 | 240 | :param currency: required for one-currency request, can be currency tag or currency id 241 | :param account_type: required for one-currency request 242 | 243 | :param zeros: required for all-currencies request, default is False (doesn't return zero balances) 244 | :type zeros: string (method argument accepts boolean for user convenience) 245 | 246 | :returns: list - list of dictionaries per currency and wallet, if all-currencies request, otherwise one dict 247 | 248 | .. code block:: python 249 | 250 | [..., 251 | { 252 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', # Account id (unique for each account of a user) 253 | 'status': 'ACCOUNT_STATUS_ACTIVE', # Currency account status (ACTIVE, DISABLED, FROZEN) 254 | 'type': 'ACCOUNT_TYPE_SPOT', # Account type (SPOT, FUTURES, WALLET, CROWDSALE) 255 | 'timestamp': 1628381804961, # Timestamp when server returned the responce 256 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', # Currency id 257 | 'available': '100.830064349760000000', # Currently available on the account (excludes blocked funds) 258 | 'blocked': '0.000000' # Currently blocked (orders placed, for example) 259 | }, 260 | ... 261 | ] 262 | 263 | """ 264 | 265 | if currency and account_type: 266 | pathParams = self._inputController(currency = currency) 267 | pathParams.update({ 268 | 'accountType': str(account_type) 269 | }) 270 | return self._APIsigned(endpoint = self.currency_balance_by_type_call.format(**pathParams)) 271 | else: 272 | queryParams = {'zeros': str(zeros).lower()} 273 | return self._APIsigned(endpoint = self.account_balances_call, params = queryParams) 274 | 275 | 276 | def getOrders(self, order_id: Optional[str] = None, pair: Optional[str] = None, active: Optional[bool] = False, 277 | limit: Optional[int] = 100, timestamp: Optional[str] = None): 278 | """Returns user orders history 279 | 280 | A request for the order by id has a priority over a request for orders by pair 281 | that itself has a priority over a request for all orders 282 | 283 | :param order_id: required for a particular order request (other arguments will be ignored) 284 | 285 | :param pair: required for request for orders in a specific pair (should be of format ***/***) 286 | :param active: optional, defaults to False (returns all orders, otherwise only active are returned) 287 | :param limit: optional, defaults to 100 288 | :type limit: string (method argument accepts integer for user convenience) 289 | :param timestamp: optional, defaults to current (orders before this timestamp are returned) 290 | 291 | :returns: list - list of dictionaries for each order, otherwise dict if only 1 order exists 292 | 293 | .. code block:: python 294 | [..., 295 | { 296 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 297 | 'status': 'ORDER_STATUS_CLOSED', 298 | 'side': 'ORDER_SIDE_SELL', 299 | 'condition': 'ORDER_CONDITION_GOOD_TILL_CANCELLED', 300 | 'type': 'ORDER_TYPE_LIMIT', 301 | 'baseCurrency': 'd286007b-03eb-454e-936f-296c4c6e3be9', 302 | 'quoteCurrency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 303 | 'clientOrderId': 'my order 1', 304 | 'price': '3.6200', 305 | 'quantity': '100.000', 306 | 'cost': '362.0000000', 307 | 'filled': '100.000', 308 | 'trader': 'a44444aa-4444-44a4-444a-44444a444aaa', # User id 309 | 'timestamp': 1624804464728 310 | }, 311 | ... 312 | ] 313 | 314 | """ 315 | 316 | if order_id: 317 | return self._APIsigned(endpoint = self.order_status_call.format(order_id)) 318 | 319 | elif pair: 320 | queryParams = { 321 | 'from': str(timestamp), 322 | 'limit': str(limit) 323 | } 324 | queryParams = {x: y for x, y in queryParams.items() if y != 'None'} 325 | 326 | pathParams = self._inputController(pair = pair) 327 | 328 | if active: 329 | return self._APIsigned(endpoint = self.order_pair_active_call.format(**pathParams), params = queryParams) 330 | else: 331 | return self._APIsigned(endpoint = self.order_pair_all_call.format(**pathParams), params = queryParams) 332 | 333 | else: 334 | queryParams = { 335 | 'from': str(timestamp), 336 | 'limit': str(limit) 337 | } 338 | queryParams = {x: y for x, y in queryParams.items() if y != 'None'} 339 | return self._APIsigned(endpoint = self.order_all_call, params = queryParams) 340 | 341 | 342 | def placeOrder(self, pair: str, side: str, client_message: str, price: float, quantity: float, 343 | timestamp: int, condition: str = 'GOOD_TILL_CANCELLED', order_type: str = 'LIMIT') -> dict: 344 | """Places an order 345 | 346 | :param pair: max 20 characters, can be any combination of currency id or currency tag (format ***/***) 347 | :param side: max 10 characters, can be "BUY", "BID", "SELL", "ASK" 348 | :param client_message: max 50 characters, write whatever you want here 349 | :param price: max 50 characters 350 | :type price: string (method argument accepts float for user convenience) 351 | :param quantity: max 50 characters 352 | :type quantity: string (method argument accepts float for user convenience) 353 | :param timestamp: required for correct signature 354 | :param condition: max 30 characters, can be "GTC", "GOOD_TILL_CANCELLED" (default), 355 | "IOC", "IMMEDIATE_OR_CANCEL", "FOK", "FILL_OR_KILL" 356 | :param order_type: max 30 characters, can be "LIMIT" (default), "MARKET" 357 | 358 | :returns: dict - dict with responce 359 | 360 | .. code block:: python 361 | 362 | { 363 | 'message': 'order accepted for placing', 364 | 'status': 'SUCCESS', 365 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa' # Order id 366 | } 367 | 368 | """ 369 | 370 | requestBodyParams = self._inputController(pair = pair, currency_name = 'baseCurrency', quote_name = 'quoteCurrency') 371 | requestBodyParams.update({ 372 | 'side': str(side.upper()), 373 | 'condition': str(condition.upper()), 374 | 'type': str(order_type.upper()), 375 | 'clientOrderId': str(client_message), 376 | 'price': str(price), 377 | 'quantity': str(quantity), 378 | 'timestamp': int(timestamp) 379 | }) 380 | return self._APIsigned(endpoint = self.order_place_call, params = requestBodyParams, request_type = 'post') 381 | 382 | 383 | def cancelOrder(self, order_id: Optional[str] = None, pair: Optional[str] = None, cancel_all: Optional[bool] = False) -> dict: 384 | """Cancels orders 385 | 386 | A request to cancel order by id has a priority over a request to cancel orders by pair 387 | that itself has a priority over a request to cancel all orders 388 | 389 | :param order_id: required for a particular order cancellation request (other arguments will be ignored) 390 | 391 | :param pair: required for cancel orders in a specific pair (should be of format ***/***) 392 | :param cancel_all: optional, defaults to False (you should explicitly set it to True to cancel all orders) 393 | 394 | :returns: dict - dict with responce 395 | 396 | .. code block:: python 397 | 398 | { 399 | 'message': 'cancellation request successfully submitted', 400 | 'status': 'SUCCESS', 401 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa' # Only returned if a specific order is cancelled 402 | } 403 | 404 | """ 405 | 406 | if order_id: 407 | requestBodyParams = {'id': str(order_id)} 408 | return self._APIsigned(endpoint = self.order_cancel_id_call, params = requestBodyParams, request_type = 'post') 409 | 410 | elif pair: 411 | pathParams = self._inputController(pair = pair) 412 | return self._APIsigned(endpoint = self.order_cancel_pair_call.format(**pathParams), request_type = 'post') 413 | 414 | elif cancel_all: 415 | return self._APIsigned(endpoint = self.order_cancel_all_call, request_type = 'post') 416 | 417 | 418 | def getTrades(self, pair: Optional[str] = None, user: bool = False, limit: Optional[int] = 100, timestamp: Optional[str] = None): 419 | """Returns user trades history 420 | 421 | A request for user trades by pair has a priority over a request all user trades 422 | that itself has a priority over a request for all trades in the market. 423 | 424 | :param user: required for request of trades by user and by user in a specific pair. Defaults to False 425 | that means that all market trades regardless the user are returned. 426 | :param pair: required for request for trade of the user in a specific pair (should be of format ***/***) 427 | :param limit: optional, defaults to 100 428 | :type limit: string (method argument accepts integer for user convenience) 429 | :param timestamp: optional, defaults to current (orders before this timestamp are returned) 430 | 431 | :returns: list - list of dictionaries for each trade, otherwise dict if only 1 trade exists 432 | 433 | .. code block:: python 434 | [..., 435 | { 436 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 437 | 'isMakerBuyer': False, 438 | 'direction': 'TRADE_DIRECTION_SELL', 439 | 'baseCurrency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 440 | 'quoteCurrency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 441 | 'price': '30000.00', 442 | 'quantity': '0.03500', 443 | 'cost': '1050.00', 444 | 'fee': '4.095000000000000000', # Omitted from public trades (given in quoteCurrency) 445 | 'order': 'a44444aa-4444-44a4-444a-44444a444aaa', # Omitted from public trades 446 | 'timestamp': 1624373391929, 447 | 'makerBuyer': False 448 | }, 449 | ... 450 | ] 451 | 452 | """ 453 | 454 | queryParams = { 455 | 'from': str(timestamp), 456 | 'limit': str(limit) 457 | } 458 | queryParams = {x: y for x, y in queryParams.items() if y != 'None'} 459 | 460 | if user and pair: # PRIVATE 461 | pathParams = self._inputController(pair = pair) 462 | return self._APIsigned(endpoint = self.trades_user_pair_call.format(**pathParams), params = queryParams) 463 | 464 | elif user: # PRIVATE 465 | return self._APIsigned(endpoint = self.trades_user_call, params = queryParams) 466 | 467 | elif pair: # PUBLIC 468 | serializeFunc = map(lambda it: it[0] + '=' + str(it[1]), queryParams.items()) 469 | queryParams = '&'.join(serializeFunc) 470 | 471 | pathParams = self._inputController(pair = pair) 472 | return requests.get(self.baseAPI + self.trades_all_call.format(**pathParams) + '?' + queryParams).json() 473 | 474 | 475 | def transferSpot(self, amount: float, currency_id: str, deposit: bool = True) -> dict: 476 | """Transfers between Spot and Wallet accounts 477 | 478 | :param amount: should be >= 0 479 | :type amount: string (method argument accepts float for user convenience) 480 | :param currency_id: apart from other methods, this one only accepts currency id (currency tag will return an error) 481 | :param deposit: defaults to True (deposit to Spot from Wallet), False means withdraw from Spot to Wallet 482 | 483 | :returns: dict - dict with the transfer result 484 | 485 | .. code block:: python 486 | 487 | { 488 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 489 | 'status': 'TRANSFER_STATUS_PENDING', 490 | 'type': 'TRANSFER_TYPE_DEPOSIT_SPOT', # Will be TRANSFER_TYPE_WITHDRAW_SPOT, if deposit set to False 491 | 'fromAccount': 'a44444aa-4444-44a4-444a-44444a444aaa', 492 | 'toAccount': 'a44444aa-4444-44a4-444a-44444a444aaa', 493 | 'transferringFunds': '10', 494 | 'usdValue': '0', 495 | 'rejectReason': '', 496 | 'timestamp': 1629537163208, 497 | 'direction': 'INTERNAL', 498 | 'method': 'TRANSFER_METHOD_UNKNOWN', 499 | 'recipient': '', 500 | 'sender': '', 501 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 502 | 'codeRequired': False, 503 | 'fromUser': 'a44444aa-4444-44a4-444a-44444a444aaa', # This is the authenticated user id 504 | 'toUser': 'a44444aa-4444-44a4-444a-44444a444aaa', # This is the authenticated user id (same as fromUser) 505 | 'fee': '0' 506 | } 507 | 508 | """ 509 | 510 | requestBodyParams = { 511 | 'value': str(amount), 512 | 'currency': str(currency_id) 513 | } 514 | if deposit: 515 | return self._APIsigned(endpoint = self.deposit_spot_call, params = requestBodyParams, request_type = 'post') 516 | 517 | elif deposit == False: 518 | return self._APIsigned(endpoint = self.withdraw_spot_call, params = requestBodyParams, request_type = 'post') 519 | 520 | 521 | def transferAccount(self, amount: float, currency_id: str, user_id: Optional[str] = None, 522 | phone: Optional[str] = None, email: Optional[str] = None) -> dict: 523 | """Transfers between external to the user accounts (within exchange) 524 | 525 | A request for transfer by user_id has a priority over the request for transfer by phone 526 | that itself has a priority over the request for transfer by email 527 | 528 | :param amount: should be >= 0 529 | :type amount: string (method argument accepts float for user convenience) 530 | :param currency_id: apart from other methods, this one only accepts currency id (currency tag will return an error) 531 | 532 | :param user_id: required for transfer by user_id, other arguments (phone and email) will be ignored 533 | :param phone: required for transfer by phone, other argument (email) will be ignored 534 | :param email: required for transfer by email, will only be used if other arguments are not present 535 | 536 | :returns: dict - dict with the transfer result 537 | 538 | .. code block:: python 539 | 540 | { 541 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 542 | 'status': 'TRANSFER_STATUS_UNVERIFIED', 543 | 'type': 'TRANSFER_TYPE_INTER_USER', 544 | 'fromAccount': None, 545 | 'toAccount': None, 546 | 'transferringFunds': '10', 547 | 'usdValue': '0', 548 | 'rejectReason': None, 549 | 'timestamp': 1629539250161, 550 | 'direction': 'OUTCOME', 551 | 'method': 'TRANSFER_METHOD_DIRECT', 552 | 'recipient': 'a44444aa-4444-44a4-444a-44444a444aaa', 553 | 'sender': 'exampleemail@email.com', 554 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 555 | 'codeRequired': False, 556 | 'fromUser': 'a44444aa-4444-44a4-444a-44444a444aaa', 557 | 'toUser': 'b44444aa-4444-44b4-444a-33333a444bbb', 558 | 'fee': None 559 | } 560 | 561 | """ 562 | 563 | requestBodyParams = { 564 | 'value': str(amount), 565 | 'currency': str(currency_id) 566 | } 567 | if user_id: 568 | requestBodyParams.update({'recipient': str(user_id)}) 569 | return self._APIsigned(endpoint = self.transfer_by_id_call, params = requestBodyParams, request_type = 'post') 570 | 571 | elif phone: 572 | requestBodyParams.update({'recipient': str(phone)}) 573 | return self._APIsigned(endpoint = self.transfer_by_phone_call, params = requestBodyParams, request_type = 'post') 574 | 575 | elif email: 576 | requestBodyParams.update({'recipient': str(email)}) 577 | return self._APIsigned(endpoint = self.transfer_by_email_call, params = requestBodyParams, request_type = 'post') 578 | 579 | else: 580 | print('No transfer method provided') 581 | 582 | 583 | def getTransfers(self, page: Optional[int] = 0, size: Optional[int] = 10) -> dict: 584 | """Returns history of user transfers without their account and to other users 585 | 586 | :param page: should be >= 0 587 | :param size: should be 1-1000 (defaults to 10), number of results returned per page 588 | 589 | :returns: dict - dict with transfers history (from the most recent to the least recent) 590 | 591 | .. code block:: python 592 | 593 | { 594 | 'hasNext': True, # Means that it has the next page 595 | 'content': [ 596 | { 597 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 598 | 'status': 'TRANSFER_STATUS_UNVERIFIED', 599 | 'type': 'TRANSFER_TYPE_INTER_USER', 600 | 'fromAccount': None, 601 | 'toAccount': None, 602 | 'transferringFunds': '10', 603 | 'usdValue': '0', 604 | 'rejectReason': None, 605 | 'timestamp': 1629539250161, 606 | 'direction': 'OUTCOME', 607 | 'method': 'TRANSFER_METHOD_DIRECT', 608 | 'recipient': 'a44444aa-4444-44a4-444a-44444a444aaa', 609 | 'sender': 'exampleemail@email.com', 610 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 611 | 'codeRequired': False, 612 | 'fromUser': 'a44444aa-4444-44a4-444a-44444a444aaa', 613 | 'toUser': 'a44444aa-4444-44a4-444a-44444a444bbb', 614 | 'fee': None 615 | }, 616 | { 617 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 618 | 'status': 'TRANSFER_STATUS_PENDING', 619 | 'type': 'TRANSFER_TYPE_DEPOSIT_SPOT', 620 | 'fromAccount': 'a44444aa-4444-44a4-444a-44444a444aaa', 621 | 'toAccount': 'a44444aa-4444-44a4-444a-44444a444aaa', 622 | 'transferringFunds': '10', 623 | 'usdValue': '0', 624 | 'rejectReason': '', 625 | 'timestamp': 1629537163208, 626 | 'direction': 'INTERNAL', 627 | 'method': 'TRANSFER_METHOD_UNKNOWN', 628 | 'recipient': '', 629 | 'sender': '', 630 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 631 | 'codeRequired': False, 632 | 'fromUser': 'a44444aa-4444-44a4-444a-44444a444aaa', 633 | 'toUser': 'a44444aa-4444-44a4-444a-44444a444aaa', 634 | 'fee': '0'000000000000000' 635 | }], 636 | 'first': True, # Means that this is the first page and there is no page before 637 | 'pageSize': 1, 638 | 'hasContent': True # Means that page is not empty 639 | } 640 | 641 | """ 642 | 643 | queryParams = { 644 | 'page': str(page), 645 | 'size': str(size) 646 | } 647 | return self._APIsigned(endpoint = self.transfer_get_all_call, params = queryParams) 648 | 649 | 650 | def makeWithdrawal(self, currency_binding_id: str, amount: float, address: str, memo: Optional[str] = None, 651 | twoFaCode: Optional[str] = None) -> dict: 652 | """Makes a withdrawal from LATOKEN 653 | 654 | :param currency_binding_id: LATOKEN internal OUTPUT binding id (each currency has a separate INPUT and OUTPUT binding per each provider) 655 | :type amount: string (method argument accepts float for user convenience) 656 | 657 | :returns: dict - dict with the transaction result 658 | 659 | .. code block:: python 660 | 661 | { 662 | 'withdrawalId': 'a44444aa-4444-44a4-444a-44444a444aaa', 663 | 'codeRequired': False, 664 | 'transaction': { 665 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 666 | 'status': 'TRANSACTION_STATUS_PENDING', 667 | 'type': 'TRANSACTION_TYPE_WITHDRAWAL', 668 | 'senderAddress': None, 669 | 'recipientAddress': 'TTccMcccM8ccMcMMc46KHzv6MeMeeeeeee', # Address to send withdrawal to 670 | 'amount': '20', 671 | 'transactionFee': '3', # Fee in sent currency 672 | 'timestamp': 1629561656227, 673 | 'transactionHash': None, # Not present in response as status is pending 674 | 'blockHeight': None, # Not present in response as status is pending 675 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 676 | 'memo': None, 677 | 'paymentProvider': None, # LATOKEN payment provider id 678 | 'requiresCode': False 679 | } 680 | } 681 | 682 | """ 683 | 684 | requestBodyParams = { 685 | 'currencyBinding': str(currency_binding_id), 686 | 'amount': str(amount), 687 | 'recipientAddress': str(address), 688 | 'memo': str(memo), 689 | 'twoFaCode': str(twoFaCode) 690 | } 691 | requestBodyParams = {x: y for x, y in requestBodyParams.items() if y != 'None'} 692 | return self._APIsigned(endpoint = self.withdrawal_request_call, params = requestBodyParams, request_type = 'post') 693 | 694 | 695 | def cancelWithdrawal(self, withdrawal_id: str) -> dict: 696 | """Cancel UNVERIFIED withdrawal 697 | 698 | :returns: dict - dict with the cancellation result 699 | 700 | """ 701 | 702 | requestBodyParams = {'id': str(withdrawal_id)} 703 | return self._APIsigned(endpoint = self.withdrawal_cancel_call, params = requestBodyParams, request_type = 'post') 704 | 705 | 706 | def confirmWithdrawal(self, withdrawal_id: str, code: str) -> dict: 707 | """Confirm UNVERIFIED withdrawal 708 | 709 | :returns: dict - dict with the confirmation result 710 | 711 | """ 712 | 713 | requestBodyParams = { 714 | 'id': str(withdrawal_id), 715 | 'confirmationCode': str(code) 716 | } 717 | return self._APIsigned(endpoint = self.withdrawal_confirmation_call, params = requestBodyParams, request_type = 'post') 718 | 719 | 720 | def resendCode(self, withdrawal_id: str) -> dict: 721 | """Resends verification code for UNVERIFIED withdrawal confirmation 722 | 723 | :returns: dict - dict with the code result 724 | 725 | """ 726 | 727 | requestBodyParams = {'id': str(withdrawal_id)} 728 | return self._APIsigned(endpoint = self.withdrawal_code_resend_call, params = requestBodyParams, request_type = 'post') 729 | 730 | 731 | def getDepositAddress(self, currency_binding_id: str) -> dict: 732 | """Returns a deposit address 733 | 734 | :param currency_binding_id: LATOKEN internal INPUT binding id 735 | 736 | :returns: dict - dict with the operation message and deposit address 737 | 738 | .. code block:: python 739 | 740 | { 741 | 'message': 'address generated', 742 | 'status': 'SUCCESS', 743 | 'depositAccount': { 744 | 'address': '0x55bb55b5b555bbb5bbb5b02555bbbb5bb5555bbb', 745 | 'memo': '' 746 | } 747 | } 748 | 749 | """ 750 | 751 | requestBodyParams = {'currencyBinding': str(currency_binding_id)} 752 | return self._APIsigned(endpoint = self.deposit_address_call, params = requestBodyParams, request_type = 'post') 753 | 754 | 755 | def getWithdrawalBindings(self) -> list: 756 | """Returns a list of OUTPUT bindings 757 | 758 | .. code block:: python 759 | 760 | [{ 761 | 'id': '230a4acf-e1c6-440d-a59f-607a5fb1c390', # Currency id 762 | 'tag': 'ARX', 763 | 'bindings': [{ 764 | 'minAmount': '144.000000000000000000', # In transacted currency 765 | 'fee': '48.000000000000000000', # In transacted currency 766 | 'percentFee': '1.000000000000000000', # In % 767 | 'providerName': 'ERC20', # Protocol that currency supports (once currency can have multiple providers) 768 | 'id': 'dbd3d401-8564-4d5d-9881-6d4b70d439b0', # OUTPUT currency binding id 769 | 'currencyProvider': '35607b89-df9e-47bd-974c-d7ca378fe4e6' 770 | }] 771 | }, 772 | ... 773 | ] 774 | 775 | """ 776 | 777 | return requests.get(self.baseAPI + self.bindings_active_call).json() 778 | 779 | 780 | def getActiveCurrencyBindings(self) -> dict: 781 | """Returns active currency bindings 782 | 783 | :returns: dict - dict with currency ids for active INPUT (deposits) and OUTPUT (withdrawals) bindings 784 | 785 | .. code block:: python 786 | 787 | { 788 | 'inputs': [ 789 | 'bf7cfeb8-2a8b-4356-a600-2b2f34c85fc9', 790 | 'ceb03f7c-2bcf-4775-9e6d-8dd95610abb7', 791 | ... 792 | ], 793 | 'outputs':[ 794 | '2e72c082-1de2-4010-bda9-d28aac11755d', 795 | '6984a559-3ec0-4f84-bd25-166fbff69a7a', 796 | ... 797 | ] 798 | } 799 | 800 | """ 801 | 802 | return self._APIsigned(endpoint = self.bindings_active_currencies_call) 803 | 804 | 805 | def getCurrencyBindings(self, currency: str) -> list: 806 | """Returns all bindings of a specific currencies 807 | 808 | :param currency: can be either currency id or currency tag 809 | 810 | :returns: list - list with dict per each currency binding (both active and inactive) 811 | 812 | .. code block:: python 813 | 814 | [{ 815 | 'id': '7d28ec03-6d1a-4586-b38d-df4b334cec1c', 816 | 'currencyProvider': '9899d208-a3e5-46bc-a594-3048b1a982bc', 817 | 'status': 'CURRENCY_BINDING_STATUS_ACTIVE', 818 | 'type': 'CURRENCY_BINDING_TYPE_OUTPUT', 819 | 'currency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 820 | 'minAmount': '0.001000000000000000', 821 | 'fee': '0.000500000000000000', 822 | 'percentFee': '1.000000000000000000', 823 | 'warning': '', 824 | 'feeCurrency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 825 | 'title': 'BTC Wallet', 826 | 'confirmationBlocks': 2, 827 | 'memoSupported': False, 828 | 'decimals': 6, 829 | 'config': {}, 830 | 'providerName': 'BTC', 831 | 'restrictedCountries': [] 832 | }, 833 | { 834 | 'id': '3a29a9cb-3f10-46e9-a8af-52c3ca8f3cab', 835 | 'currencyProvider': '9899d208-a3e5-46bc-a594-3048b1a982bc', 836 | 'status': 'CURRENCY_BINDING_STATUS_ACTIVE', 837 | 'type': 'CURRENCY_BINDING_TYPE_INPUT', 838 | 'currency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 839 | 'minAmount': '0.000500000000000000', 840 | 'fee': '0', 841 | 'percentFee': '0', 842 | 'warning': '', 843 | 'feeCurrency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 844 | 'title': 'BTC Wallet ', 845 | 'confirmationBlocks': 2, 846 | 'memoSupported': False, 847 | 'decimals': 6, 848 | 'config': {}, 849 | 'providerName': 'BTC', 850 | 'restrictedCountries': [] 851 | }, 852 | { 853 | 'id': 'e225e53e-6756-4f2b-bc2c-ebc4dc60b2d9', 854 | 'currencyProvider': 'bf169c61-26cd-49a0-a6e1-a8781d1d4058', 855 | 'status': 'CURRENCY_BINDING_STATUS_DISABLED', 856 | 'type': 'CURRENCY_BINDING_TYPE_INPUT', 857 | 'currency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 858 | 'minAmount': '0.000300000000000000', 859 | 'fee': '0', 860 | 'percentFee': '0', 861 | 'warning': '', 862 | 'feeCurrency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 863 | 'title': 'BTCB Wallet BEP-20', 864 | 'confirmationBlocks': 15, 865 | 'memoSupported': False, 866 | 'decimals': 18, 867 | 'config': { 868 | 'address': '0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c' 869 | }, 870 | 'providerName': 'BSC_TOKEN', 871 | 'restrictedCountries': [] 872 | }, 873 | { 874 | 'id': 'c65cd18f-6d9c-40f3-acca-072d4d1977fd', 875 | 'currencyProvider': 'bf169c61-26cd-49a0-a6e1-a8781d1d4058', 876 | 'status': 'CURRENCY_BINDING_STATUS_DISABLED', 877 | 'type': 'CURRENCY_BINDING_TYPE_OUTPUT', 878 | 'currency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 879 | 'minAmount': '0.000300000000000000', 880 | 'fee': '0.000300000000000000', 881 | 'percentFee': '1.000000000000000000', 882 | 'warning': '', 883 | 'feeCurrency': '92151d82-df98-4d88-9a4d-284fa9eca49f', 884 | 'title': 'BTCB Wallet BEP-20', 885 | 'confirmationBlocks': 15, 886 | 'memoSupported': False, 887 | 'decimals': 18, 888 | 'config': { 889 | 'address': '0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c' 890 | }, 891 | 'providerName': 'BSC_TOKEN', 892 | 'restrictedCountries': [] 893 | } 894 | ] 895 | 896 | """ 897 | 898 | pathParams = self._inputController(currency = currency) 899 | return self._APIsigned(endpoint = self.bindings_currency_call.format(**pathParams)) 900 | 901 | 902 | def getTransactions(self, transaction_id: Optional[str] = None, page: Optional[int] = 0, size: Optional[int] = 10) -> dict: 903 | """Returns a history of user transactions 904 | 905 | A request for transaction by id pas a priority over the request for all transactions 906 | 907 | :param transaction_id: required, if request a specific transaction 908 | :param page: should be >= 0 909 | :param size: should be 1-1000 (defaults to 10), number of results returned per page 910 | 911 | ..code block:: python 912 | 913 | { 914 | 'hasNext': True, 915 | 'content': [{ 916 | 'id': 'a44444aa-4444-44a4-444a-44444a444aaa', 917 | 'status': 'TRANSACTION_STATUS_CONFIRMED', 918 | 'type': 'TRANSACTION_TYPE_WITHDRAWAL', 919 | 'senderAddress': '', 920 | 'recipientAddress': 'TTccMcccM8ccMcMMc46KHzv6MeMeeeeeee', 921 | 'amount': '20.000000000000000000', 922 | 'transactionFee': '3.000000000000000000', 923 | 'timestamp': 1629561656406, 924 | 'transactionHash': '900a0000000a0cc647a2aa10555a555555233aaa065a5a6369600000000000', 925 | 'blockHeight': 0, 926 | 'currency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 927 | 'memo': None, 928 | 'paymentProvider': '4732c7cc-5f53-4f12-a757-96c7c6ba2e8e', 929 | 'requiresCode': False 930 | }, 931 | { 932 | ... 933 | }], 934 | 'first': True, 935 | 'pageSize': 1, 936 | 'hasContent': True 937 | } 938 | 939 | """ 940 | 941 | if transaction_id: 942 | pathParams = {'id': str(transaction_id)} 943 | return self._APIsigned(endpoint = self.transaction_by_id_call.format(**pathParams)) 944 | 945 | else: 946 | queryParams = { 947 | 'page': str(page), 948 | 'size': str(size) 949 | } 950 | return self._APIsigned(endpoint = self.transaction_all_call, params = queryParams) 951 | 952 | 953 | def getCurrencies(self, currency: Optional[str] = None, get_all: bool = True): 954 | """Returns currency data 955 | 956 | :param currency: can be either currency tag or currency id 957 | 958 | :returns: dict - dict with currency information (if requesting one currency), list with currencies otherwise 959 | 960 | .. code block:: python 961 | 962 | [{ 963 | 'id': '92151d82-df98-4d88-9a4d-284fa9eca49f', 964 | 'status': 'CURRENCY_STATUS_ACTIVE', 965 | 'type': 'CURRENCY_TYPE_CRYPTO', 966 | 'name': 'Bitcoin', 967 | 'tag': 'BTC', 968 | 'description': '', 969 | 'logo': '', 970 | 'decimals': 8, 971 | 'created': 1572912000000, 972 | 'tier': 1, 973 | 'assetClass': 'ASSET_CLASS_UNKNOWN', 974 | 'minTransferAmount': 0 975 | }, 976 | ... 977 | ] 978 | 979 | """ 980 | 981 | if currency: 982 | pathParams = self._inputController(currency = currency) 983 | return requests.get(self.baseAPI + self.currency_call.format(**pathParams)).json() 984 | 985 | elif get_all: 986 | return requests.get(self.baseAPI + self.active_currency_call).json() 987 | 988 | 989 | def getQuoteCurrencies(self) -> list: 990 | """Returns quote currencies 991 | 992 | :returns: list - list of currencies used as quote on LATOKEN 993 | 994 | .. code block:: python 995 | 996 | [ 997 | '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 998 | '92151d82-df98-4d88-9a4d-284fa9eca49f', 999 | '620f2019-33c0-423b-8a9d-cde4d7f8ef7f', 1000 | '34629b4b-753c-4537-865f-4b62ff1a31d6', 1001 | '707ccdf1-af98-4e09-95fc-e685ed0ae4c6', 1002 | 'd286007b-03eb-454e-936f-296c4c6e3be9' 1003 | ] 1004 | 1005 | """ 1006 | 1007 | return requests.get(self.baseAPI + self.quote_currency_call).json() 1008 | 1009 | 1010 | def getActivePairs(self) -> list: 1011 | """Returns active pairs 1012 | 1013 | :returns: list - list of active pairs information 1014 | 1015 | .. code block:: python 1016 | 1017 | [..., 1018 | { 1019 | 'id': '752896cd-b656-4d9a-814d-b97686246350', 1020 | 'status': 'PAIR_STATUS_ACTIVE', 1021 | 'baseCurrency': 'c9f5bf11-92ec-461b-877c-49e32f133e13', 1022 | 'quoteCurrency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 1023 | 'priceTick': '0.000000000010000000', 1024 | 'priceDecimals': 11, 1025 | 'quantityTick': '0.010000000', 1026 | 'quantityDecimals': 2, 1027 | 'costDisplayDecimals': 9, 1028 | 'created': 1625153024491, 1029 | 'minOrderQuantity': '0', 1030 | 'maxOrderCostUsd': '999999999999999999', 1031 | 'minOrderCostUsd': '0', 1032 | 'externalSymbol': '' 1033 | }, 1034 | ...] 1035 | 1036 | """ 1037 | 1038 | return requests.get(self.baseAPI + self.active_pairs_call).json() 1039 | 1040 | 1041 | def getOrderbook(self, pair: str, limit: Optional[int] = 1000) -> dict: 1042 | """Returns orderbook for a specific pair 1043 | 1044 | :param pair: can be either currency tag or currency id (should of format ***/***) 1045 | :param limit: number or price levels returned in bids and asks, defaults to 1000 1046 | 1047 | :returns: dict - dict with asks and bids that contain information for each price level 1048 | 1049 | ..code block:: python 1050 | 1051 | { 1052 | 'ask': 1053 | [{ 1054 | 'price': '46566.69', 1055 | 'quantity': '0.0081', 1056 | 'cost': '377.190189', 1057 | 'accumulated': '377.190189' 1058 | }], 1059 | 'bid': 1060 | [{ 1061 | 'price': '46561.91', 1062 | 'quantity': '0.0061', 1063 | 'cost': '284.027651', 1064 | 'accumulated': '284.027651 1065 | }], 1066 | 'totalAsk': '3.4354', # In base currency 1067 | 'totalBid': '204967.154792' # In quote currency 1068 | } 1069 | 1070 | """ 1071 | 1072 | pathParams = self._inputController(pair = pair) 1073 | queryParams = f'limit={limit}' 1074 | return requests.get(self.baseAPI + self.orderbook_call.format(**pathParams) + '?' + queryParams).json() 1075 | 1076 | 1077 | def getTickers(self, pair: Optional[str] = None, get_all: bool = True): 1078 | """Returns tickers 1079 | 1080 | :param pair: can be either currency tag or currency id (should of format ***/***) 1081 | :param get_all: defaults to True (returns tickers for all pairs) 1082 | 1083 | :returns: list - list of dicts with pairs' tickers (by default), dict with one pair ticker otherwise 1084 | 1085 | .. code block:: python 1086 | 1087 | [..., 1088 | { 1089 | 'symbol': 'SNX/USDT', 1090 | 'baseCurrency': 'c4624bdb-1148-440d-803d-7b55031d481d', 1091 | 'quoteCurrency': '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 1092 | 'volume24h': '1177568.232859658500000000', 1093 | 'volume7d': '1177568.232859658500000000', 1094 | 'change24h': '0', 1095 | 'change7d': '0', 1096 | 'lastPrice': '12.02347082' 1097 | }, 1098 | ... 1099 | ] 1100 | 1101 | """ 1102 | 1103 | if pair: 1104 | pathParams = self._inputController(pair = pair) 1105 | return requests.get(self.baseAPI + self.tickers_per_pair_call.format(**pathParams)).json() 1106 | 1107 | elif get_all: 1108 | return requests.get(self.baseAPI + self.tickers_call).json() 1109 | 1110 | 1111 | def getFeeLevels(self) -> list: 1112 | """Returns fee levels 1113 | 1114 | :returns: list - list of dicts with maker and taker fee per each volume level (30d accumulated volume) 1115 | 1116 | .. code block:: python 1117 | 1118 | [ 1119 | {'makerFee': '0.0049', 'takerFee': '0.0049', 'volume': '0'}, 1120 | {'makerFee': '0.0039', 'takerFee': '0.0039', 'volume': '10000'}, 1121 | {'makerFee': '0.0029', 'takerFee': '0.0029', 'volume': '50000'}, 1122 | {'makerFee': '0.0012', 'takerFee': '0.0019', 'volume': '100000'}, 1123 | {'makerFee': '0.0007', 'takerFee': '0.0011', 'volume': '250000'}, 1124 | {'makerFee': '0.0006', 'takerFee': '0.0009', 'volume': '1000000'}, 1125 | {'makerFee': '0.0004', 'takerFee': '0.0007', 'volume': '2500000'}, 1126 | {'makerFee': '0.0002', 'takerFee': '0.0005', 'volume': '10000000'}, 1127 | {'makerFee': '0', 'takerFee': '0.0004', 'volume': '20000000'} 1128 | ] 1129 | 1130 | """ 1131 | 1132 | return requests.get(self.baseAPI + self.fee_levels_call).json() 1133 | 1134 | 1135 | def getFeeScheme(self, pair: str, user: Optional[bool] = False) -> dict: 1136 | """Returns fee scheme for a particular pair 1137 | 1138 | :param user: defaults to False (returns fee scheme per pair for all users, for particular user otherwise) 1139 | 1140 | .. code block:: python 1141 | 1142 | { 1143 | 'makerFee': '0.004900000000000000', # Proportion (not %) 1144 | 'takerFee': '0.004900000000000000', # Proportion (not %) 1145 | 'type': 'FEE_SCHEME_TYPE_PERCENT_QUOTE', 1146 | 'take': 'FEE_SCHEME_TAKE_PROPORTION' 1147 | } 1148 | 1149 | """ 1150 | 1151 | pathParams = self._inputController(pair = pair) 1152 | 1153 | if pair and user: 1154 | return self._APIsigned(endpoint = self.fee_scheme_par_pair_and_user_call.format(**pathParams)) 1155 | 1156 | elif pair: 1157 | return requests.get(self.baseAPI + self.fee_scheme_per_pair_call.format(**pathParams)).json() 1158 | 1159 | 1160 | def getChart(self, pair: Optional[str] = None): 1161 | """Returns charts 1162 | 1163 | :param pair: can be either currency tag or currency id (should of format ***/***) 1164 | 1165 | :returns: if no arguments specified, the dict is returned with currency ids as keys 1166 | and list of 169 weekly prices as values, otherwise a single list is retured 1167 | 1168 | ..code block:: python 1169 | 1170 | { 1171 | '30a1032d-1e3e-4c28-8ca7-b60f3406fc3e': [..., 1.375e-05, 1.382e-05, 1.358e-05, ...], 1172 | 'd8958071-c13f-40fb-bd54-d2f64c36e15b': [..., 0.0001049, 0.000104, 0.0001045, ...], 1173 | ... 1174 | } 1175 | 1176 | """ 1177 | 1178 | if pair: 1179 | pathParams = self._inputController(pair = pair) 1180 | return requests.get(self.baseAPI + self.weekly_chart_by_pair_call.format(**pathParams)).json() 1181 | 1182 | else: 1183 | return requests.get(self.baseAPI + self.weekly_chart_call).json() 1184 | 1185 | 1186 | def getCandles(self, start: str, end: str, pair: str = None, resolution: str = '1h') -> dict: 1187 | """Returns charts 1188 | 1189 | :param pair: can be either currency tag or currency id (should of format ***/***) 1190 | :param resolution: can be 1m, 1h (default), 4h, 6h, 12h, 1d, 7d or 1w, 30d or 1M 1191 | :param start: timestamp in seconds (included in responce) 1192 | :param end: timestamp in seconds (not included in responce) 1193 | 1194 | :returns: dict - the dict with open, close, low, high, time, volume as keys and list of values 1195 | 1196 | ..code block:: python 1197 | 1198 | { 1199 | "o":["49926.320000000000000000", ..., "49853.580000000000000000"], 1200 | "c":["50193.230000000000000000", ..., "49948.57"], 1201 | "l":["49777.000000000000000000", ...,"49810.200000000000000000"], 1202 | "h":["50555.000000000000000000", ...,"49997.350000000000000000"], 1203 | "t":[1630800000, ..., 1630828800], 1204 | "v":["2257782.696156400000000000", ..., "811505.269468400000000000"], 1205 | "s":"ok" 1206 | } 1207 | 1208 | """ 1209 | 1210 | pathParams = self._inputController(pair = pair) 1211 | pathParams.update({ 1212 | 'resolution': str(resolution), 1213 | 'from': str(start), 1214 | 'to': str(end) 1215 | }) 1216 | return requests.get(self.baseAPI + self.candles_call.format(**pathParams)).json() 1217 | 1218 | 1219 | # WEBSOCKETS 1220 | 1221 | def _WSsigned(self) -> dict: 1222 | timestamp = str(int(float(time()) * 1000)) 1223 | 1224 | # We should sign a timestamp in milliseconds by the api secret 1225 | signature = hmac.new( 1226 | self.apiSecret, 1227 | timestamp.encode('ascii'), 1228 | hashlib.sha512 1229 | ) 1230 | return { 1231 | 'X-LA-APIKEY': self.apiKey, 1232 | 'X-LA-SIGNATURE': signature.hexdigest(), 1233 | 'X-LA-DIGEST': 'HMAC-SHA512', 1234 | 'X-LA-SIGDATA': timestamp 1235 | } 1236 | 1237 | 1238 | async def connect(self, streams: list = topics, signed: bool = False, on_message = None): 1239 | 1240 | ws=websocket.create_connection(self.baseWS) 1241 | msg = stomper.Frame() 1242 | msg.cmd = "CONNECT" 1243 | msg.headers = { 1244 | "accept-version": "1.1", 1245 | "heart-beat": "0,0" 1246 | } 1247 | 1248 | # If the request is for a private stream, then add signature headers to headers 1249 | if signed: 1250 | msg.headers.update(self._WSsigned()) 1251 | 1252 | ws.send(msg.pack()) 1253 | ws.recv() 1254 | 1255 | # Subscribing to streams, subscription id is assigned as an index in topics list 1256 | for stream in streams: 1257 | msg = stomper.subscribe(stream, streams.index(stream), ack="auto") 1258 | ws.send(msg) 1259 | 1260 | # Telling the application to execute a business logic on each message from the server 1261 | while True: 1262 | message = ws.recv() 1263 | message = stomper.unpack_frame(message.decode()) 1264 | await on_message(message) 1265 | 1266 | 1267 | def run(self, connect): 1268 | loop = asyncio.get_event_loop() 1269 | loop.run_until_complete(connect) 1270 | 1271 | 1272 | # Websocket streams 1273 | def streamAccounts(self) -> dict: 1274 | """Returns all user currency balances 1275 | 1276 | :returns: dict - dict with all user balances by wallet type 1277 | 1278 | .. code block:: python 1279 | 1280 | { 1281 | 'cmd': 'MESSAGE', 1282 | 'headers': { 1283 | 'destination': '/user/a44444aa-4444-44a4-444a-44444a444aaa/v1/account', 1284 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1285 | 'content-length': '22090', 1286 | 'subscription': '0' 1287 | }, 1288 | 'body': '{ 1289 | "payload":[ 1290 | { 1291 | "id":"a44444aa-4444-44a4-444a-44444a444aaa", 1292 | "status":"ACCOUNT_STATUS_ACTIVE", 1293 | "type":"ACCOUNT_TYPE_FUTURES", 1294 | "timestamp":1594198124804, 1295 | "currency":"ebf4eb8a-06ec-4955-bd81-85a7860764b9", 1296 | "available":"31.265578482497400000", 1297 | "blocked":"0", 1298 | "user":"a44444aa-4444-44a4-444a-44444a444aaa" 1299 | }, 1300 | ... 1301 | ], 1302 | "nonce":0, 1303 | "timestamp":1630172200117 1304 | }' 1305 | } 1306 | 1307 | """ 1308 | 1309 | user_id = self.getUserInfo()['id'] 1310 | pathParams = {'user': str(user_id)} 1311 | accounts_topics = self.account_stream.format(**pathParams) 1312 | return self.topics.append(accounts_topics) 1313 | 1314 | 1315 | def streamTransactions(self): 1316 | """Stream returns user transactions (to/from outside LATOKEN) history, function only returns a subscription endpoint 1317 | 1318 | .. code block:: python 1319 | 1320 | { 1321 | 'cmd': 'MESSAGE', 1322 | 'headers': { 1323 | 'destination': '/user/a44444aa-4444-44a4-444a-44444a444aaa/v1/transaction', 1324 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1325 | 'content-length': '13608', 1326 | 'subscription': '0' 1327 | }, 1328 | 'body': '{ 1329 | "payload":[ 1330 | { 1331 | "id":"a44444aa-4444-44a4-444a-44444a444aaa", 1332 | "status":"TRANSACTION_STATUS_CONFIRMED", 1333 | "type":"TRANSACTION_TYPE_WITHDRAWAL", 1334 | "senderAddress":"", 1335 | "recipientAddress":"TTccMcccM8ccMcMMc46KHzv6MeMeeeeeee", 1336 | "transferredAmount":"20.000000000000000000", 1337 | "timestamp":1629561656404, 1338 | "transactionHash":"000000rrrrr000c647a27f7f7f7777ff9052338bf065000000fffffff7iiii88", 1339 | "blockHeight":0, 1340 | "transactionFee":"3.000000000000000000", 1341 | "currency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", 1342 | "user":"a44444aa-4444-44a4-444a-44444a444aaa", 1343 | "paymentProvider":"4732c7cc-5f53-4f12-a757-96c7c6ba2e8e", 1344 | "requiresCode":false 1345 | }, 1346 | ... 1347 | ], 1348 | "nonce":0, 1349 | "timestamp":1630182304943 1350 | }' 1351 | } 1352 | 1353 | """ 1354 | 1355 | user_id = self.getUserInfo()['id'] 1356 | pathParams = {'user': str(user_id)} 1357 | transactions_topics = self.transactions_stream.format(**pathParams) 1358 | return self.topics.append(transactions_topics) 1359 | 1360 | 1361 | def streamTransfers(self): 1362 | """Stream returns user transfers (within LATOKEN) history, function only returns a subscription endpoint 1363 | 1364 | .. code block:: python 1365 | 1366 | 1367 | """ 1368 | 1369 | user_id = self.getUserInfo()['id'] 1370 | pathParams = {'user': str(user_id)} 1371 | transfers_topics = self.transfers_stream.format(**pathParams) 1372 | return self.topics.append(transfers_topics) 1373 | 1374 | 1375 | def streamOrders(self): 1376 | """Stream returns user orders history, function only returns a subscription endpoint 1377 | 1378 | .. code block:: python 1379 | 1380 | { 1381 | 'cmd': 'MESSAGE', 1382 | 'headers': { 1383 | 'destination': '/user/a44444aa-4444-44a4-444a-44444a444aaa/v1/order', 1384 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1385 | 'content-length': '27246', 1386 | 'subscription': '0' 1387 | }, 1388 | 'body': '{ 1389 | "payload":[ 1390 | { 1391 | "id":"a44444aa-4444-44a4-444a-44444a444aaa", 1392 | "user":"a44444aa-4444-44a4-444a-44444a444aaa", 1393 | "changeType":"ORDER_CHANGE_TYPE_UNCHANGED", 1394 | "status":"ORDER_STATUS_CANCELLED", 1395 | "side":"ORDER_SIDE_BUY", 1396 | "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", 1397 | "type":"ORDER_TYPE_LIMIT", 1398 | "baseCurrency":"92151d82-df98-4d88-9a4d-284fa9eca49f", 1399 | "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", 1400 | "clientOrderId":"test1", 1401 | "price":"39000", 1402 | "quantity":"0.001", 1403 | "cost":"39.000000000000000000", 1404 | "filled":"0.000000000000000000", 1405 | "deltaFilled":"0", 1406 | "timestamp":1629039302489, 1407 | "rejectError":null, 1408 | "rejectComment":null 1409 | }, 1410 | ... 1411 | ], 1412 | "nonce":0, 1413 | "timestamp":1630180898279 1414 | }' 1415 | } 1416 | 1417 | """ 1418 | 1419 | user_id = self.getUserInfo()['id'] 1420 | pathParams = {'user': str(user_id)} 1421 | orders_topics = self.orders_stream.format(**pathParams) 1422 | return self.topics.append(orders_topics) 1423 | 1424 | 1425 | def streamCurrencies(self): 1426 | """Stream returns currencies information, function only returns a subscription endpoint 1427 | 1428 | .. code block:: python 1429 | 1430 | { 1431 | 'cmd': 'MESSAGE', 1432 | 'headers': { 1433 | 'destination': '/v1/currency', 1434 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1435 | 'content-length': '265186', 1436 | 'subscription': '0' 1437 | }, 1438 | 'body': '{ 1439 | "payload":[ 1440 | { 1441 | "id":"af544ebf-630b-4bac-89c1-35ee5caca50b", 1442 | "status":"CURRENCY_STATUS_ACTIVE", 1443 | "type":"CURRENCY_TYPE_CRYPTO", 1444 | "name":"Javvy Crypto Solution", 1445 | "description":"", 1446 | "decimals":18, 1447 | "tag":"JVY", 1448 | "logo":"", 1449 | "minTransferAmount":"", 1450 | "assetClass":"ASSET_CLASS_UNKNOWN" 1451 | }, 1452 | ... 1453 | ], 1454 | "nonce":0, 1455 | "timestamp":1630180614787 1456 | }' 1457 | } 1458 | 1459 | """ 1460 | 1461 | return self.topics.append(self.currencies_stream) 1462 | 1463 | 1464 | def streamPairs(self): 1465 | """Stream returns pairs information, function only returns a subscription endpoint 1466 | 1467 | .. code block:: python 1468 | 1469 | { 1470 | 'cmd': 'MESSAGE', 1471 | 'headers': { 1472 | 'destination': '/v1/pair', 1473 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1474 | 'content-length': '293954', 1475 | 'subscription': '0' 1476 | }, 1477 | 'body': '{ 1478 | "payload":[ 1479 | { 1480 | "id":"c49baa32-88f0-4f7b-adca-ab66afadc75e", 1481 | "status":"PAIR_STATUS_ACTIVE", 1482 | "baseCurrency":"59c87258-af77-4c15-ae12-12da8cadc545", 1483 | "quoteCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", 1484 | "priceTick":"0.000000010000000000", 1485 | "quantityTick":"1.000000000", 1486 | "costDisplayDecimals":8, 1487 | "quantityDecimals":0, 1488 | "priceDecimals":8, 1489 | "externalSymbol":"", 1490 | "minOrderQuantity":"0.000000000000000000", 1491 | "maxOrderCostUsd":"999999999999999999.000000000000000000", 1492 | "minOrderCostUsd":"0.000000000000000000" 1493 | }, 1494 | ... 1495 | ], 1496 | "nonce":0, 1497 | "timestamp":1630180179490 1498 | }' 1499 | } 1500 | 1501 | """ 1502 | 1503 | return self.topics.append(self.pairs_stream) 1504 | 1505 | 1506 | def streamTickers(self): 1507 | """Stream returns tickers for all pairs, function only returns a subscription endpoint 1508 | 1509 | .. code block:: python 1510 | 1511 | { 1512 | 'cmd': 'MESSAGE', 1513 | 'headers': { 1514 | 'destination': '/v1/ticker', 1515 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1516 | 'content-length': '260547', 1517 | 'subscription': '0' 1518 | }, 1519 | 'body': '{ 1520 | "payload":[ 1521 | { 1522 | "baseCurrency":"1cbcbd8f-74e6-4476-aaa1-e883a467ee3f", 1523 | "quoteCurrency":"92151d82-df98-4d88-9a4d-284fa9eca49f", 1524 | "volume24h":"0", 1525 | "volume7d":"0", 1526 | "change24h":"0", 1527 | "change7d":"0", 1528 | "lastPrice":"0.0000012" 1529 | }, 1530 | ... 1531 | ], 1532 | "nonce":1, 1533 | "timestamp":1630179152495 1534 | }' 1535 | } 1536 | 1537 | """ 1538 | 1539 | return self.topics.append(self.ticker_all_stream) 1540 | 1541 | 1542 | def streamBook(self, pairs: list): 1543 | """Stream returns orderbook of a specific pair, function only returns a subscription endpoint 1544 | 1545 | :param pairs: should consist of currency_ids only, otherwise will return nothing, pair should be of format ***/*** 1546 | 1547 | :returns: dict - dict for each requested pair as a separate message 1548 | 1549 | .. code block:: python 1550 | 1551 | { 1552 | 'cmd': 'MESSAGE', 1553 | 'headers': { 1554 | 'destination': '/v1/book/620f2019-33c0-423b-8a9d-cde4d7f8ef7f/0c3a106d-bde3-4c13-a26e-3fd2394529e5', 1555 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1556 | 'content-length': '184', 1557 | 'subscription': '1' 1558 | }, 1559 | 'body': '{ 1560 | "payload":{ 1561 | "ask":[], 1562 | "bid":[ 1563 | { 1564 | "price":"3218.07", 1565 | "quantityChange":"1.63351", 1566 | "costChange":"5256.7495257", 1567 | "quantity":"1.63351", 1568 | "cost":"5256.7495257" 1569 | }, 1570 | ... 1571 | ] 1572 | }, 1573 | "nonce":1, 1574 | "timestamp":1630178170860 1575 | }' 1576 | } 1577 | 1578 | """ 1579 | 1580 | pathParams = [self._inputController(pair = pair) for pair in pairs] 1581 | book_topics = [self.book_stream.format(**pathParam) for pathParam in pathParams] 1582 | return [self.topics.append(book_topic) for book_topic in book_topics] 1583 | 1584 | 1585 | def streamPairTickers(self, pairs: list): 1586 | """Stream returns pairs' volume and price changes, function only returns a subscription endpoint 1587 | 1588 | :param pairs: should consist of currency_ids only, otherwise will return nothing, pair should be of format ***/*** 1589 | 1590 | :returns: dict - dict for each requested pair as a separate message 1591 | 1592 | .. code block:: python 1593 | 1594 | { 1595 | 'cmd': 'MESSAGE', 1596 | 'headers': { 1597 | 'destination': '/v1/ticker/620f2019-33c0-423b-8a9d-cde4d7f8ef7f/0c3a106d-bde3-4c13-a26e-3fd2394529e5', 1598 | 'message-id': '0a44444aa-4444-44a4-444a-44444a444aaa', 1599 | 'content-length': '277', 1600 | 'subscription': '1' 1601 | }, 1602 | 'body': '{ 1603 | "payload":{ 1604 | "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", 1605 | "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", 1606 | "volume24h":"37874830.2350945", 1607 | "volume7d":"183513541.8285953", 1608 | "change24h":"0.28", 1609 | "change7d":"-0.71", 1610 | "lastPrice":"3239" 1611 | }, 1612 | "nonce":0, 1613 | "timestamp":1630177120904 1614 | }' 1615 | } 1616 | 1617 | """ 1618 | 1619 | pathParams = [self._inputController(pair = pair) for pair in pairs] 1620 | pair_tickers_topics = [self.tickers_pair_stream.format(**pathParam) for pathParam in pathParams] 1621 | return [self.topics.append(pair_tickers_topic) for pair_tickers_topic in pair_tickers_topics] 1622 | 1623 | 1624 | def streamTrades(self, pairs: list): 1625 | """Stream returns market trades, function only returns a subscription endpoint 1626 | 1627 | :param pairs: should consist of currency_ids only, otherwise will return an empty message, pair should be of format ***/*** 1628 | 1629 | .. code block:: python 1630 | 1631 | { 1632 | 'cmd': 'MESSAGE', 1633 | 'headers': { 1634 | 'destination': '/v1/trade/620f2019-33c0-423b-8a9d-cde4d7f8ef7f/0c3a106d-bde3-4c13-a26e-3fd2394529e5', 1635 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1636 | 'content-length': '30105', 1637 | 'subscription': '1' 1638 | }, 1639 | 'body': '{ 1640 | "payload":[ 1641 | { 1642 | "id":"a44444aa-4444-44a4-444a-44444a444aaa", 1643 | "timestamp":1630175902267, 1644 | "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", 1645 | "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", 1646 | "direction":null, 1647 | "price":"3243.14", 1648 | "quantity":"0.44887", 1649 | "cost":"1455.748251800000000000", 1650 | "order":null, 1651 | "makerBuyer":false 1652 | }, 1653 | ... 1654 | ], 1655 | "nonce":0, 1656 | "timestamp":1630175907898 1657 | }' 1658 | } 1659 | 1660 | """ 1661 | 1662 | pathParams = [self._inputController(pair = pair) for pair in pairs] 1663 | trades_topics = [self.trades_stream.format(**pathParam) for pathParam in pathParams] 1664 | return [self.topics.append(trades_topic) for trades_topic in trades_topics] 1665 | 1666 | 1667 | def streamRates(self, pairs: list): 1668 | """Stream returns rate for specified pairs, function only returns a subscription endpoint 1669 | 1670 | :param pairs: can consist of currency_ids or currency tag, pair should be of format ***/*** 1671 | 1672 | :returns: dict - dict for each requested pair as a separate message 1673 | 1674 | .. code block:: python 1675 | 1676 | { 1677 | 'cmd': 'MESSAGE', 1678 | 'headers': { 1679 | 'destination': '/v1/rate/BTC/USDT', # Returns the format you requested the pair in (can be mixute of currency id and quote) 1680 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1681 | 'content-length': '87', 1682 | 'subscription': '0' 1683 | }, 1684 | 'body': '{ 1685 | "payload":[ 1686 | { 1687 | "symbol":"BTC/USDT", 1688 | "rate":48984.99 1689 | } 1690 | ], 1691 | "nonce":0, 1692 | "timestamp":1630173083252 1693 | }' 1694 | } 1695 | 1696 | """ 1697 | 1698 | pathParams = [self._inputController(pair = pair) for pair in pairs] 1699 | rates_topics = [self.rates_stream.format(**pathParam) for pathParam in pathParams] 1700 | return [self.topics.append(rates_topic) for rates_topic in rates_topics] 1701 | 1702 | 1703 | def streamQuoteRates(self, quotes: list): 1704 | """Stream returns rates for all currencies quoted to specified quotes, function only returns a subscription endpoint 1705 | 1706 | :param quotes: is a list of quote currencies that can be either currency tag or currency id (should of format ***/***) 1707 | 1708 | .. code block:: python 1709 | 1710 | { 1711 | 'cmd': 'MESSAGE', 1712 | 'headers': { 1713 | 'destination': '/v1/rate/USDT', 1714 | 'message-id': 'a44444aa-4444-44a4-444a-44444a444aaa', 1715 | 'content-length': '49986', 1716 | 'subscription': '0' 1717 | }, 1718 | 'body': '{ 1719 | "payload":[ 1720 | {"symbol":"USDN/USDT","rate":0.9988}, 1721 | {"symbol":"USDJ/USDT","rate":0.98020001}, 1722 | ..., 1723 | {"symbol":"VTHO/USDT","rate":0.011324} 1724 | ], 1725 | "nonce":0, 1726 | "timestamp":1630171197332 1727 | }' 1728 | } 1729 | 1730 | """ 1731 | 1732 | pathParams = [self._inputController(currency = quote, currency_name = 'quote') for quote in quotes] 1733 | quote_rates_topics = [self.rates_quote_stream.format(**pathParam) for pathParam in pathParams] 1734 | return [self.topics.append(quote_rates_topic) for quote_rates_topic in quote_rates_topics] 1735 | 1736 | 1737 | 1738 | -------------------------------------------------------------------------------- /latoken/enums.py: -------------------------------------------------------------------------------- 1 | CANDLE_INTERVAL_1MIN = '1m' 2 | CANDLE_INTERVAL_1HOUR = '1h' 3 | CANDLE_INTERVAL_4HOURS = '4h' 4 | CANDLE_INTERVAL_6HOURS = '6h' 5 | CANDLE_INTERVAL_12HOURS = '12h' 6 | CANDLE_INTERVAL_1D = '1d' 7 | CANDLE_INTERVAL_7D = '7d' 8 | CANDLE_INTERVAL_30D = '1M' 9 | 10 | SIDE_BUY = 'BUY' 11 | SIDE_SELL = 'SELL' 12 | 13 | ORDER_STATUS_PLACED = 'ORDER_STATUS_PLACED' 14 | ORDER_STATUS_CLOSED = 'ORDER_STATUS_CLOSED' 15 | ORDER_STATUS_CANCELLED = 'ORDER_STATUS_CANCELLED' 16 | 17 | ORDER_TYPE_LIMIT = 'LIMIT' 18 | ORDER_TYPE_MARKET = 'MARKET' 19 | 20 | ORDER_CONDITION_GTC = 'GOOD_TILL_CANCELLED' 21 | ORDER_CONDITION_IOC = 'IMMEDIATE_OR_CANCEL' 22 | ORDER_CONDITION_FOK = 'FILL_OR_KILL' -------------------------------------------------------------------------------- /latoken/helpers.py: -------------------------------------------------------------------------------- 1 | from latoken.client import LatokenClient 2 | from typing import Optional 3 | 4 | 5 | def currencyConverter(currency_ids: Optional[list] = None, currency_tags: Optional[list] = None) -> list: 6 | """Converts currency ids into tickers and vice versa 7 | 8 | .. code block:: python 9 | 10 | Input: [ 11 | '0c3a106d-bde3-4c13-a26e-3fd2394529e5', 12 | '92151d82-df98-4d88-9a4d-284fa9eca49f', 13 | '620f2019-33c0-423b-8a9d-cde4d7f8ef7f', 14 | '34629b4b-753c-4537-865f-4b62ff1a31d6', 15 | '707ccdf1-af98-4e09-95fc-e685ed0ae4c6', 16 | 'd286007b-03eb-454e-936f-296c4c6e3be9' 17 | ] 18 | 19 | Output: [ 20 | 'USDT', 21 | 'BTC', 22 | 'ETH', 23 | 'TRX', 24 | 'LA', 25 | 'EOS' 26 | ] 27 | 28 | """ 29 | 30 | response = LatokenClient().getCurrencies() 31 | 32 | if currency_ids: 33 | mapper = {i['id']: i['tag'] for i in response} 34 | return [mapper[i] for i in currency_ids] 35 | elif currency_tags: 36 | mapper = {i['tag']: i['id'] for i in response} 37 | return [mapper[i] for i in currency_tags] 38 | else: 39 | return print('No list of currency_ids or currency_tags provided') -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | with open("README.rst", "r") as desc: 5 | long_description = desc.read() 6 | 7 | setup( 8 | name = 'latoken-api-v2-python-client', 9 | packages = find_packages(include = ['latoken']), 10 | version = '0.2.1', 11 | description = 'LATOKEN REST API and STOMP Websocket python implementation', 12 | long_description = long_description, 13 | long_description_content_type = "text/x-rst", 14 | author = 'LATOKEN', 15 | license = 'MIT', 16 | url = 'https://github.com/LATOKEN/latoken-api-v2-python-client', 17 | install_requires = ['requests', 'stomper', 'websocket-client'], 18 | keywords = 'latoken exchange rest websockets api crypto bitcoin trading', 19 | classifiers = [ 20 | 'Intended Audience :: Developers', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.7', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Programming Language :: Python :: 3.9', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Operating System :: OS Independent' 27 | ], 28 | ) 29 | --------------------------------------------------------------------------------