├── .gitignore ├── FTX ├── __init__.py ├── client.py ├── constants.py └── helpers.py ├── License ├── README.md ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *egg-info/ 3 | .env 4 | .venv 5 | -------------------------------------------------------------------------------- /FTX/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | An unofficial Python wrapper for the FTX exchange API 3 | """ 4 | -------------------------------------------------------------------------------- /FTX/client.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import hashlib 3 | import hmac 4 | import json 5 | from time import sleep 6 | from typing import List, NewType, Optional, Dict, Union 7 | import urllib 8 | from urllib.parse import urlencode 9 | 10 | from expiringdict import ExpiringDict 11 | import requests 12 | 13 | from . import constants 14 | from . import helpers 15 | 16 | 17 | ListOfDicts = NewType("ListOfDicts", List[dict]) 18 | # shape: 19 | # {'bids': [[2259.5, 0.0013], 20 | # ...], 21 | # 'asks': [[2299.5, 1.8906], 22 | # ...], 23 | # } 24 | BidsAndAsks = NewType("BidsAndAsks", Dict[str, List[List[float]]]) 25 | 26 | 27 | class Invalid(Exception): 28 | pass 29 | 30 | 31 | class DoesntExist(Exception): 32 | pass 33 | 34 | 35 | class Client: 36 | _requests = ExpiringDict( 37 | max_len=constants.RATE_LIMIT_PER_SECOND, max_age_seconds=60 38 | ) 39 | 40 | def __init__( 41 | self, key: str, secret: str, subaccount: Optional[str] = None, timeout: int = 30 42 | ): 43 | self._api_key = key 44 | self._api_secret = secret 45 | self._api_subaccount = subaccount 46 | self._api_timeout = timeout 47 | 48 | def _build_headers(self, scope: str, method: str, endpoint: str, query: dict): 49 | endpoint = f"/api/{endpoint}" 50 | 51 | headers = { 52 | "Accept": "application/json", 53 | "User-Agent": "FTX-Trader/1.0", 54 | } 55 | 56 | if scope.lower() == "private": 57 | nonce = str(helpers.get_current_timestamp()) 58 | payload = f"{nonce}{method.upper()}{endpoint}" 59 | if method == "GET" and query: 60 | payload += "?" + urlencode(query) 61 | elif query: 62 | payload += json.dumps(query) 63 | sign = hmac.new( 64 | bytes(self._api_secret, "utf-8"), 65 | bytes(payload, "utf-8"), 66 | hashlib.sha256, 67 | ).hexdigest() 68 | 69 | headers.update( 70 | { 71 | # This header is REQUIRED to send JSON data. 72 | "Content-Type": "application/json", 73 | "FTX-KEY": self._api_key, 74 | "FTX-SIGN": sign, 75 | "FTX-TS": nonce, 76 | } 77 | ) 78 | 79 | if self._api_subaccount: 80 | headers.update( 81 | { 82 | # If you want to access a subaccount 83 | "FTX-SUBACCOUNT": urllib.parse.quote(self._api_subaccount) 84 | } 85 | ) 86 | 87 | return headers 88 | 89 | def _build_url(self, scope: str, method: str, endpoint: str, query: dict) -> str: 90 | if scope.lower() == "private": 91 | url = f"{constants.PRIVATE_API_URL}/{endpoint}" 92 | else: 93 | url = f"{constants.PUBLIC_API_URL}/{endpoint}" 94 | 95 | if method == "GET": 96 | return f"{url}?{urlencode(query, True, '/[]')}" if len(query) > 0 else url 97 | else: 98 | return url 99 | 100 | def _send_request(self, method: str, endpoint: str, query: Optional[dict] = None): 101 | # .values() because of a bug in len(ExpiringDict) 102 | if len(self._requests.values()) == constants.RATE_LIMIT_PER_SECOND: 103 | print( 104 | "waiting half a second because there have been " 105 | f"{constants.RATE_LIMIT_PER_SECOND} requests in the past second.\n" 106 | f"{method}: '{endpoint}' query='{query}'" 107 | ) 108 | sleep(.5) 109 | query = query or {} 110 | 111 | scope = "public" 112 | if any(endpoint.startswith(substr) for substr in constants.PRIVATE_ENDPOINTS): 113 | scope = "private" 114 | 115 | # Build header first 116 | headers = self._build_headers(scope, method, endpoint, query) 117 | 118 | # Build final url here 119 | url = self._build_url(scope, method, endpoint, query) 120 | 121 | try: 122 | if method == "GET": 123 | response = requests.get(url, headers=headers).json() 124 | elif method == "POST": 125 | response = requests.post(url, headers=headers, json=query).json() 126 | elif method == "DELETE": 127 | response = requests.delete(url, headers=headers, json=query).json() 128 | except Exception as e: 129 | print("[x] Error: {}".format(e.args[0])) 130 | finally: 131 | # increment the number of requests in the past second 132 | self._requests[dt.datetime.now()] = None 133 | 134 | if "result" in response: 135 | return response["result"] 136 | elif "error" in response: 137 | raise DoesntExist(response["error"]) 138 | else: 139 | return response 140 | 141 | def _GET(self, endpoint, query=None): 142 | return self._send_request("GET", endpoint, query) 143 | 144 | def _POST(self, endpoint, query=None): 145 | return self._send_request("POST", endpoint, query) 146 | 147 | def _DELETE(self, endpoint, query=None): 148 | return self._send_request("DELETE", endpoint, query) 149 | 150 | # Public API 151 | def get_markets(self) -> ListOfDicts: 152 | """ 153 | https://docs.ftx.com/#markets 154 | """ 155 | return self._GET("markets") 156 | 157 | def get_market(self, pair: str) -> dict: 158 | """ 159 | https://docs.ftx.com/#get-single-market 160 | :param pair: the trading pair to query 161 | """ 162 | return self._GET(f"markets/{pair.upper()}") 163 | 164 | def get_orderbook(self, pair: str, depth: int = 20) -> BidsAndAsks: 165 | """ 166 | https://docs.ftx.com/#get-orderbook 167 | 168 | :param pair: the trading pair to query 169 | :param depth: the price levels depth to query (max: 100 default: 20) 170 | :return: a dict contains asks and bids data 171 | """ 172 | if depth > 100 or depth < 20: 173 | raise Invalid("depth must be between 20 and 100") 174 | 175 | return self._GET(f"markets/{pair}/orderbook", {"depth": depth}) 176 | 177 | def get_recent_trades( 178 | self, 179 | pair: str, 180 | limit: Optional[int] = constants.DEFAULT_LIMIT, 181 | start_time: Optional[int] = None, 182 | end_time: Optional[int] = None, 183 | ) -> ListOfDicts: 184 | """ 185 | https://docs.ftx.com/#get-trades 186 | 187 | :param pair: the trading pair to query 188 | :param limit: the records limit to query 189 | :param start_time: the target period after an Epoch time in seconds 190 | :param end_time: the target period before an Epoch time in seconds 191 | :return: a list contains all completed orders in exchange 192 | """ 193 | query = helpers.build_query( 194 | limit=limit, start_time=start_time, end_time=end_time 195 | ) 196 | 197 | return self._GET(f"markets/{pair}/trades", query) 198 | 199 | def get_k_line( 200 | self, 201 | pair: str, 202 | resolution: int = constants.DEFAULT_K_LINE_RESOLUTION, 203 | limit: Optional[int] = constants.DEFAULT_LIMIT, 204 | start_time: Optional[int] = None, 205 | end_time: Optional[int] = None, 206 | ) -> ListOfDicts: 207 | """ 208 | https://docs.ftx.com/#get-historical-prices 209 | 210 | :param pair: the trading pair to query 211 | :param resolution: the time period of K line in seconds 212 | :param limit: the records limit to query 213 | :param start_time: the target period after an Epoch time in seconds 214 | :param end_time: the target period before an Epoch time in seconds 215 | :return: a list contains all OHLC prices in exchange 216 | """ 217 | if resolution not in constants.VALID_K_LINE_RESOLUTIONS: 218 | raise Invalid( 219 | f"resolution must be in {', '.join(constants.VALID_K_LINE_RESOLUTIONS)}" 220 | ) 221 | 222 | query = helpers.build_query( 223 | limit=limit, start_time=start_time, end_time=end_time, resolution=resolution 224 | ) 225 | 226 | return self._GET(f"markets/{pair}/candles", query) 227 | 228 | def get_futures(self) -> ListOfDicts: 229 | """ 230 | https://docs.ftx.com/#list-all-futures 231 | 232 | :return: a list contains all available futures 233 | """ 234 | 235 | return self._GET("/futures") 236 | 237 | def get_perpetual_futures(self) -> ListOfDicts: 238 | """ 239 | https://docs.ftx.com/#list-all-futures 240 | 241 | :return: a list contains all available perpetual futures 242 | """ 243 | return [future for future in self.get_futures() if future["perpetual"]] 244 | 245 | def get_future(self, pair: str) -> dict: 246 | """ 247 | https://docs.ftx.com/#get-future 248 | 249 | :param pair: the trading pair to query 250 | :return: a list contains single future info 251 | """ 252 | 253 | return self._GET(f"futures/{pair.upper()}") 254 | 255 | def get_future_stats(self, pair: str) -> dict: 256 | """ 257 | https://docs.ftx.com/#get-future-stats 258 | 259 | :param pair: the trading pair to query 260 | :return: a list contains stats of a future 261 | """ 262 | 263 | return self._GET(f"futures/{pair.upper()}/stats") 264 | 265 | def get_funding_rates(self) -> ListOfDicts: 266 | """ 267 | https://docs.ftx.com/#get-funding-rates 268 | 269 | :return: a list contains all funding rate of perpetual futures 270 | """ 271 | 272 | return self._GET("funding_rates") 273 | 274 | def get_etf_future_index(self, index): 275 | """ 276 | # TODO: Note that this only applies to index futures, e.g. ALT/MID/SHIT/EXCH/DRAGON. 277 | https://docs.ftx.com/#get-index-weights 278 | 279 | :param index: the trading index to query 280 | :return: a list contains all component coins in ETF Future 281 | """ 282 | 283 | return self._GET(f"indexes/{index}/weights") 284 | 285 | def get_expired_futures(self) -> ListOfDicts: 286 | """ 287 | https://docs.ftx.com/#get-expired-futures 288 | 289 | :return: a list contains all expired futures 290 | """ 291 | 292 | return self._GET("expired_futures") 293 | 294 | def get_index_k_line( 295 | self, 296 | index: str, 297 | resolution: int = constants.DEFAULT_K_LINE_RESOLUTION, 298 | limit: Optional[int] = constants.DEFAULT_LIMIT, 299 | start_time: Optional[int] = None, 300 | end_time: Optional[int] = None, 301 | ) -> ListOfDicts: 302 | """ 303 | https://docs.ftx.com/#get-historical-index 304 | 305 | :param index: the trading index to query 306 | :param resolution: the time period of K line in seconds 307 | :param limit: the records limit to query 308 | :param start_time: the target period after an Epoch time in seconds 309 | :param end_time: the target period before an Epoch time in seconds 310 | :return: a list contains all OHLC prices of etf index in exchange 311 | """ 312 | if resolution not in constants.VALID_K_LINE_RESOLUTIONS: 313 | raise Invalid( 314 | f"resolution must be in {', '.join(constants.VALID_K_LINE_RESOLUTIONS)}" 315 | ) 316 | 317 | query = helpers.build_query( 318 | resolution=resolution, limit=limit, start_time=start_time, end_time=end_time 319 | ) 320 | 321 | return self._GET(f"indexes/{index}/candles", query) 322 | 323 | # Private API 324 | 325 | def get_account_info(self) -> dict: 326 | """ 327 | https://docs.ftx.com/#get-account-information 328 | 329 | :return: a dict contains all personal profile and positions information 330 | """ 331 | 332 | return self._GET("account") 333 | 334 | def get_positions(self, showAvgPrice: bool = False) -> Union[list, ListOfDicts]: 335 | """ 336 | https://docs.ftx.com/#get-positions 337 | 338 | :param showAvgPrice: display AvgPrice or not 339 | :return: a dict contains all positions 340 | """ 341 | 342 | return self._GET("positions", {"showAvgPrice": showAvgPrice}) 343 | 344 | def get_subaccounts(self) -> Union[list, ListOfDicts]: 345 | """ 346 | https://docs.ftx.com/#get-all-subaccounts 347 | 348 | :return: a list contains all subaccounts 349 | """ 350 | 351 | return self._GET("subaccounts") 352 | 353 | def get_subaccount_balances(self, name: str) -> ListOfDicts: 354 | """ 355 | https://docs.ftx.com/#get-subaccount-balances 356 | 357 | :param name: the subaccount name to query 358 | :return: a list contains subaccount balances 359 | """ 360 | 361 | return self._GET(f"subaccounts/{name}/balances") 362 | 363 | def get_wallet_coins(self) -> ListOfDicts: 364 | """ 365 | https://docs.ftx.com/#get-coins 366 | 367 | :return: a list contains all coins in wallet 368 | """ 369 | 370 | return self._GET("wallet/coins") 371 | 372 | def get_balances(self) -> ListOfDicts: 373 | """ 374 | https://docs.ftx.com/#get-balances 375 | 376 | :return: a list contains current account balances 377 | """ 378 | 379 | return self._GET("wallet/balances") 380 | 381 | def get_balance(self, coin: str) -> dict: 382 | """ 383 | https://docs.ftx.com/#get-balances 384 | 385 | :params coin: the coin of balance 386 | :return: a list contains current account single balance 387 | """ 388 | 389 | balance_coin = [ 390 | balance for balance in self.get_balances() if balance["coin"] == coin 391 | ] 392 | 393 | if not balance_coin: 394 | return None 395 | 396 | return balance_coin[0] 397 | 398 | def get_all_balances(self) -> Dict[str, ListOfDicts]: 399 | """ 400 | https://docs.ftx.com/#get-balances-of-all-accounts 401 | 402 | :return: a list contains all accounts balances 403 | """ 404 | 405 | return self._GET("wallet/all_balances") 406 | 407 | def get_deposit_address(self, coin: str, chain: Optional[str] = None) -> dict: 408 | """ 409 | https://docs.ftx.com/#get-deposit-address 410 | 411 | :param currency: the specific coin to endpoint 412 | :param chain: the blockchain deposit from, 413 | should be 'omni' or 'erc20', 'trx', 'bep2', or 'sol' 414 | :return: a list contains deposit address 415 | """ 416 | if chain and chain not in constants.VALID_CHAINS: 417 | raise Invalid(f"'chain' must be in {', '.join(constants.VALID_CHAINS)}") 418 | 419 | query = {} 420 | if chain is not None: 421 | query["method"] = chain 422 | 423 | return self._GET(f"wallet/deposit_address/{coin.upper()}", query) 424 | 425 | def get_deposit_history( 426 | self, 427 | limit: Optional[int] = constants.DEFAULT_LIMIT, 428 | start_time: Optional[int] = None, 429 | end_time: Optional[int] = None, 430 | ) -> Union[list, ListOfDicts]: 431 | """ 432 | https://docs.ftx.com/#get-deposit-history 433 | 434 | :param limit: the records limit to query 435 | :param start_time: the target period after an Epoch time in seconds 436 | :param end_time: the target period before an Epoch time in seconds 437 | :return: a list contains deposit history 438 | """ 439 | query = helpers.build_query( 440 | end_time=end_time, start_time=start_time, limit=limit 441 | ) 442 | 443 | return self._GET("wallet/deposits", query) 444 | 445 | def get_withdrawal_history( 446 | self, 447 | limit: Optional[int] = constants.DEFAULT_LIMIT, 448 | start_time: Optional[int] = None, 449 | end_time: Optional[int] = None, 450 | ) -> Union[list, ListOfDicts]: 451 | """ 452 | https://docs.ftx.com/#get-withdrawal-history 453 | 454 | :param limit: the records limit to query 455 | :param start_time: the target period after an Epoch time in seconds 456 | :param end_time: the target period before an Epoch time in seconds 457 | :return: a list contains withdraw history 458 | """ 459 | query = helpers.build_query( 460 | end_time=end_time, start_time=start_time, limit=limit 461 | ) 462 | 463 | return self._GET("wallet/withdrawals", query) 464 | 465 | def get_wallet_airdrops( 466 | self, 467 | limit: Optional[int] = constants.DEFAULT_LIMIT, 468 | start_time: Optional[int] = None, 469 | end_time: Optional[int] = None, 470 | ) -> Union[list, ListOfDicts]: 471 | """ 472 | https://docs.ftx.com/#get-airdrops 473 | 474 | :param limit: the records limit to query 475 | :param start_time: the target period after an Epoch time in seconds 476 | :param end_time: the target period before an Epoch time in seconds 477 | :return: a list contains airdrop history 478 | """ 479 | 480 | query = helpers.build_query( 481 | end_time=end_time, start_time=start_time, limit=limit 482 | ) 483 | 484 | return self._GET("wallet/airdrops", query) 485 | 486 | def get_funding_payments( 487 | self, 488 | coin: str = None, 489 | start_time: Optional[int] = None, 490 | end_time: Optional[int] = None, 491 | ): 492 | """ 493 | https://docs.ftx.com/#funding-payments 494 | 495 | :param coin: the trading coin to query 496 | :param start_time: the target period after an Epoch time in seconds 497 | :param end_time: the target period before an Epoch time in seconds 498 | :return: a list contains all funding payments of perpetual future 499 | """ 500 | 501 | query = helpers.build_query(end_time=end_time, start_time=start_time) 502 | 503 | if coin is not None: 504 | query["future"] = f"{coin.upper()}-PERP" 505 | 506 | return self._GET("funding_payments", query) 507 | 508 | def get_fills( 509 | self, 510 | pair: str, 511 | limit: Optional[int] = constants.DEFAULT_LIMIT, 512 | start_time: Optional[int] = None, 513 | end_time: Optional[int] = None, 514 | order: Optional[str] = None, 515 | orderId: Optional[int] = None, 516 | ) -> Union[list, ListOfDicts]: 517 | """ 518 | https://docs.ftx.com/#fills 519 | 520 | :param pair: the trading pair to query 521 | :param limit: the records limit to query 522 | :param start_time: the target period after an Epoch time in seconds 523 | :param end_time: the target period before an Epoch time in seconds 524 | :param order: sort the bill by created time, default is descending, supply 'asc' to receive fills in ascending order of time 525 | :param orderId: the id of the order 526 | :return: a list contains all bills 527 | """ 528 | 529 | if order not in (None, "asc"): 530 | raise Invalid("Please supply either None or 'asc' for `order`") 531 | 532 | query = helpers.build_query( 533 | limit=limit, 534 | start_time=start_time, 535 | end_time=end_time, 536 | order=order, 537 | orderId=orderId, 538 | ) 539 | query["market"] = pair 540 | 541 | return self._GET("fills", query) 542 | 543 | def get_open_orders(self, pair=None): 544 | """ 545 | https://docs.ftx.com/?python#get-open-orders 546 | 547 | :param pair: the trading pair to query 548 | :return: a list contains all open orders 549 | """ 550 | query = {"market": pair} if pair is not None else {} 551 | 552 | return self._GET("orders", query) 553 | 554 | def get_order_history(self, pair=None, start_time=None, end_time=None, limit=None): 555 | """ 556 | https://docs.ftx.com/?python#get-order-history 557 | 558 | :param pair: the trading pair to query 559 | :param start_time: the target period after an Epoch time in seconds 560 | :param end_time: the target period before an Epoch time in seconds 561 | :param limit: the records limit to query 562 | :return: a list contains all history orders 563 | """ 564 | query = helpers.build_query( 565 | end_time=end_time, start_time=start_time, limit=limit 566 | ) 567 | query["market"] = pair 568 | 569 | return self._GET("orders/history", query) 570 | 571 | def get_open_trigger_orders(self, pair=None, type_=None): 572 | """ 573 | https://docs.ftx.com/?python#get-open-trigger-orders 574 | 575 | :param pair: the trading pair to query 576 | :param _type: type of trigger order, should only be stop, trailing_stop, or take_profit 577 | :return: a list contains all open trigger orders 578 | """ 579 | 580 | query = {} 581 | 582 | if pair is not None: 583 | query["market"] = pair 584 | if type_ is not None: 585 | query["type"] = type_ 586 | 587 | return self._GET("conditional_orders", query) 588 | 589 | def get_trigger_order_triggers(self, _orderId): 590 | """ 591 | https://docs.ftx.com/?python#get-open-trigger-orders 592 | 593 | :param _orderId: the id of the order 594 | :return: a list contains trigger order triggers 595 | """ 596 | 597 | return self._GET(f"conditional_orders/{_orderId}/triggers") 598 | 599 | def get_trigger_order_history( 600 | self, 601 | pair=None, 602 | start_time=None, 603 | end_time=None, 604 | side=None, 605 | type_=None, 606 | orderType=None, 607 | limit=None, 608 | ): 609 | """ 610 | https://docs.ftx.com/?python#get-trigger-order-history 611 | 612 | :param pair: the trading pair to query 613 | :param start_time: the target period after an Epoch time in seconds 614 | :param end_time: the target period before an Epoch time in seconds 615 | :param side: the trading side, should only be buy or sell 616 | :param type_: type of trigger order, should only be stop, trailing_stop, or take_profit 617 | :param orderType: the order type, should only be limit or market 618 | :param limit: the records limit to query 619 | :return: a list contains all history trigger orders 620 | """ 621 | 622 | query = helpers.build_query( 623 | start_time=start_time, 624 | end_time=end_time, 625 | side=side, 626 | orderType=orderType, 627 | limit=limit, 628 | ) 629 | 630 | if pair is not None: 631 | query["market"] = pair 632 | if type_ is not None: 633 | query["type"] = type_ 634 | 635 | return self._GET("conditional_orders/history", query) 636 | 637 | def get_order_status(self, orderId): 638 | """ 639 | https://docs.ftx.com/#get-order-status 640 | 641 | :param orderId: the order ID 642 | :return a list contains status of the order 643 | """ 644 | 645 | return self._GET(f"orders/{orderId}") 646 | 647 | def get_order_status_by_clientId(self, clientId): 648 | """ 649 | https://docs.ftx.com/#get-order-status-by-client-id 650 | 651 | :param clientOrderId: the client order ID 652 | :return a list contains status of the order 653 | """ 654 | 655 | return self._GET(f"orders/by_client_id/{clientId}") 656 | 657 | # Private API (Write) 658 | def create_subaccount(self, name): 659 | """ 660 | https://docs.ftx.com/?python#create-subaccount 661 | 662 | :param name: new subaccount name 663 | :return: a list contains new subaccount info 664 | """ 665 | 666 | return self._POST("subaccounts", {"nickname": name}) 667 | 668 | def change_subaccount_name(self, name, newname): 669 | """ 670 | https://docs.ftx.com/?python#change-subaccount-name 671 | 672 | :param name: current subaccount name 673 | :param newname: new nickname of subaccount 674 | :return: a list contains status 675 | """ 676 | 677 | query = {"nickname": name, "newNickname": newname} 678 | 679 | return self._POST("subaccounts/update_name", query) 680 | 681 | def delete_subaccount(self, name): 682 | """ 683 | https://docs.ftx.com/?python#delete-subaccount 684 | 685 | :param name: the nickname wanna delete 686 | :return: a list contains status 687 | """ 688 | 689 | return self._DELETE("subaccounts", {"nickname": name}) 690 | 691 | # TODO: Endpoint Error > Not allowed with internal-transfers-disabled permissions 692 | def transfer_balances(self, coin, size, source, destination): 693 | """ 694 | https://docs.ftx.com/?python#transfer-between-subaccounts 695 | 696 | :param coin: the transfering coin to query 697 | :param size: the size wanna transfer to query 698 | :param source: the name of the source subaccount. 699 | Use null or 'main' for the main account 700 | :param destination: the name of the destination subaccount. 701 | Use null or 'main' for the main account 702 | :return: a list contains status 703 | """ 704 | 705 | query = { 706 | "coin": coin, 707 | "size": size, 708 | "source": source, 709 | "destination": destination, 710 | } 711 | 712 | return self._POST("subaccounts/transfer", query) 713 | 714 | def change_account_leverage(self, leverage): 715 | """ 716 | https://docs.ftx.com/?python#change-account-leverage 717 | 718 | :param leverage: desired acccount-wide leverage setting 719 | :return: a list contains status 720 | """ 721 | 722 | return self._POST("account/leverage", {"leverage": leverage}) 723 | 724 | def create_order( 725 | self, 726 | pair, 727 | side, 728 | price, 729 | _type, 730 | size, 731 | reduceOnly=False, 732 | ioc=False, 733 | postOnly=False, 734 | clientId=None, 735 | ): 736 | """ 737 | https://docs.ftx.com/?python#place-order 738 | 739 | :param pair: the trading pair to query. 740 | e.g. "BTC/USD" for spot, "XRP-PERP" for futures 741 | :param side: the trading side, should only be buy or sell 742 | :param price: the order price, Send null for market orders. 743 | :param _type: type of order, should only be limit or market 744 | :param size: the amount of the order for the trading pair 745 | :param reduceOnly: only reduce position, default is false (future only) 746 | :param ioc: immediate or cancel order, default is false 747 | :param postOnly: always maker, default is false 748 | :param clientId: client order id 749 | :return: a list contains all info about new order 750 | """ 751 | 752 | query = { 753 | "market": pair, 754 | "side": side, 755 | "price": price, 756 | "type": _type, 757 | "size": size, 758 | "reduceOnly": reduceOnly, 759 | "ioc": ioc, 760 | "postOnly": postOnly, 761 | } 762 | 763 | if clientId is not None: 764 | query["clientId"] = clientId 765 | 766 | return self._POST("orders", query) 767 | 768 | def create_trigger_order( 769 | self, 770 | pair, 771 | side, 772 | triggerPrice, 773 | size, 774 | orderPrice=None, 775 | _type="stop", 776 | reduceOnly=False, 777 | retryUntilFilled=True, 778 | ): 779 | """ 780 | https://docs.ftx.com/?python#place-trigger-order 781 | 782 | :param pair: the trading pair to query. 783 | e.g. "BTC/USD" for spot, "XRP-PERP" for futures 784 | :param side: the trading side, should only be buy or sell 785 | :param triggerPrice: the order trigger price 786 | :param orderPrice: the order price, 787 | order type is limit if this is specified; otherwise market 788 | :param size: the amount of the order for the trading pair 789 | :param _type: type of order, should only be stop, 790 | trailingStop or takeProfit, default is stop 791 | :param reduceOnly: only reduce position, default is false (future only) 792 | :param retryUntilFilled: Whether or not to keep re-triggering until filled. 793 | optional, default true for market orders 794 | :return: a list contains all info about new trigger order 795 | """ 796 | 797 | query = { 798 | "market": pair, 799 | "side": side, 800 | "triggerPrice": triggerPrice, 801 | "size": size, 802 | "type": _type, 803 | "reduceOnly": reduceOnly, 804 | "retryUntilFilled": retryUntilFilled, 805 | } 806 | 807 | if orderPrice is not None: 808 | query["orderPrice"] = orderPrice 809 | 810 | return self._POST("conditional_orders", query) 811 | 812 | # TODO: Either price or size must be specified 813 | def modify_order( 814 | self, orderId: str, price: Optional[float] = None, size=None, clientId=None 815 | ): 816 | """ 817 | https://docs.ftx.com/#modify-order 818 | 819 | Please note that the order's queue priority will be reset, 820 | and the order ID of the modified order will be different 821 | from that of the original order. Also note: this is implemented 822 | as cancelling and replacing your order. There's a chance that 823 | the order meant to be cancelled gets filled and its replacement still gets 824 | placed. 825 | 826 | :param orderId: the order ID 827 | :param price: the modify price 828 | :param size: the modify amount of the order for the trading pair 829 | :param clientId: client order id 830 | :return a list contains all info after modify the order 831 | """ 832 | query = helpers.build_query(clientId=clientId, size=size, price=price) 833 | 834 | return self._POST(f"orders/{orderId}/modify", query) 835 | 836 | # TODO: Either price or size must be specified 837 | def modify_order_by_clientId( 838 | self, clientOrderId, price=None, size=None, clientId=None 839 | ): 840 | """ 841 | https://docs.ftx.com/#modify-order-by-client-id 842 | Please note that the order's queue priority will be reset, 843 | and the order ID of the modified order will be different 844 | from that of the original order. Also note: this is implemented 845 | as cancelling and replacing your order. There's a chance that 846 | the order meant to be cancelled gets filled and its replacement still gets 847 | placed. 848 | 849 | :param clientOrderId: the client order ID 850 | :param price: the modify price 851 | :param size: the modify amount of the order for the trading pair 852 | :param clientId: client order id 853 | :return a list contains all info after modify the order 854 | """ 855 | 856 | query = helpers.build_query(clientId=clientId, size=size, price=price) 857 | 858 | return self._POST(f"orders/by_client_id/{clientOrderId}/modify", query) 859 | 860 | def modify_trigger_order( 861 | self, orderId, _type, size, triggerPrice=None, orderPrice=None, trailValue=None 862 | ): 863 | """ 864 | https://docs.ftx.com/#modify-trigger-order 865 | 866 | Please note that the order ID of the modified order will be different from that of the original order. 867 | 868 | 869 | :param orderId: the order ID 870 | :param _type: type of order, 871 | should only be stop, trailingStop or takeProfit, default is stop 872 | :param size: the modify amount of the order for the trading pair 873 | :param triggerPrice: the modify trigger price 874 | :param orderPrice: the order price, 875 | order type is limit if this is specified; otherwise market 876 | :param trailValue: negative for sell orders; positive for buy orders 877 | :return a list contains all info after modify the trigger order 878 | """ 879 | 880 | if _type in ("stop", "takeProfit"): 881 | query = { 882 | "size": size, 883 | "triggerPrice": triggerPrice, 884 | } 885 | if orderPrice is not None: 886 | query["orderPrice"] = orderPrice 887 | else: 888 | query = {"size": size, "trailValue": trailValue} 889 | 890 | return self._POST(f"conditional_orders/{orderId}/modify", query) 891 | 892 | def cancel_order(self, orderId): 893 | """ 894 | https://docs.ftx.com/#cancel-order 895 | 896 | :param orderId: the order ID 897 | :return a list contains result 898 | """ 899 | 900 | return self._DELETE(f"orders/{orderId}") 901 | 902 | def cancel_order_by_clientID(self, clientId): 903 | """ 904 | https://docs.ftx.com/#cancel-order-by-client-id 905 | 906 | :param clientOrderId: the client order ID 907 | :return a list contains result 908 | """ 909 | 910 | return self._DELETE(f"orders/by_client_id/{clientId}") 911 | 912 | def cancel_trigger_order(self, orderId): 913 | """ 914 | https://docs.ftx.com/#cancel-open-trigger-order 915 | 916 | :param orderId: the order ID 917 | :return a list contains result 918 | """ 919 | 920 | return self._DELETE(f"conditional_orders/{orderId}") 921 | 922 | def cancel_all_orders( 923 | self, pair=None, conditionalOrdersOnly=False, limitOrdersOnly=False 924 | ): 925 | """ 926 | https://docs.ftx.com/#cancel-all-orders 927 | 928 | :param pair: the trading pair to query. 929 | :param conditionalOrdersOnly: default False. 930 | :param limitOrdersOnly: default False. 931 | :return a list contains result 932 | """ 933 | query = { 934 | "conditionalOrdersOnly": conditionalOrdersOnly, 935 | "limitOrdersOnly": limitOrdersOnly, 936 | } 937 | 938 | if pair is not None: 939 | query["market"] = pair 940 | 941 | return self._DELETE("orders", query) 942 | 943 | # SRM Stake 944 | 945 | def get_srm_stake_history(self): 946 | """ 947 | https://docs.ftx.com/#get-stakes 948 | 949 | :return a list contains srm stake history 950 | """ 951 | 952 | return self._GET("srm_stakes/stakes") 953 | 954 | def get_srm_unstake_history(self): 955 | """ 956 | https://docs.ftx.com/#unstake-request 957 | 958 | :return a list contains srm unstake history 959 | """ 960 | 961 | return self._GET("srm_stakes/unstake_requests") 962 | 963 | def get_srm_stake_balances(self): 964 | """ 965 | https://docs.ftx.com/#get-stake-balances 966 | 967 | :return a list contains actively staked, 968 | scheduled for unstaking and lifetime rewards balances 969 | """ 970 | 971 | return self._GET("srm_stakes/balances") 972 | 973 | def get_srm_stake_rewards_history(self): 974 | """ 975 | https://docs.ftx.com/#get-staking-rewards 976 | 977 | :return a list contains srm staking rewards 978 | """ 979 | 980 | return self._GET("srm_stakes/staking_rewards") 981 | 982 | def srm_unstake(self, coin, size): 983 | """ 984 | https://docs.ftx.com/#unstake-request-2 985 | 986 | :param coin: the staking coin to query 987 | :param size: the amount of the request for the stake coin 988 | :return a list contains result 989 | """ 990 | 991 | query = {"coin": coin, "size": size} 992 | 993 | return self._POST("srm_stakes/unstake_requests", query) 994 | 995 | def cancel_srm_unstake(self, stakeId): 996 | """ 997 | https://docs.ftx.com/#cancel-unstake-request 998 | 999 | :param stakeId: the id of staking request 1000 | :return a list contains result 1001 | """ 1002 | 1003 | return self._DELETE(f"srm_stakes/unstake_requests/{stakeId}") 1004 | 1005 | def srm_stake(self, coin, size): 1006 | """ 1007 | https://docs.ftx.com/#stake-request 1008 | 1009 | :param coin: the staking coin to query 1010 | :param size: the amount of the request for the stake coin 1011 | :return a list contains result 1012 | """ 1013 | 1014 | query = {"coin": coin, "size": size} 1015 | 1016 | return self._POST("srm_stakes/stakes", query) 1017 | 1018 | def get_margin_lending_rates(self): 1019 | """ 1020 | https://docs.ftx.com/#get-lending-rates 1021 | :return a list contains lending rates 1022 | (include estimate rates and previous rates) 1023 | """ 1024 | 1025 | return self._GET("spot_margin/lending_rates") 1026 | 1027 | def set_margin_lending_offer(self, coin, size, rate): 1028 | """ 1029 | https://docs.ftx.com/#submit-lending-offer 1030 | :param coin: the lending coin to query 1031 | :param size: the amount of the request for the lend coin (Cancel for 0) 1032 | :param rate: the rate wanna offer 1033 | :return a list contains result 1034 | """ 1035 | 1036 | query = {"coin": coin, "size": size, "rate": rate} 1037 | 1038 | return self._POST("spot_margin/offers", query) 1039 | -------------------------------------------------------------------------------- /FTX/constants.py: -------------------------------------------------------------------------------- 1 | PUBLIC_API_URL = "https://ftx.com/api" 2 | PRIVATE_API_URL = "https://ftx.com/api" 3 | DEFAULT_LIMIT = None 4 | DEFAULT_K_LINE_RESOLUTION = 14_400 5 | VALID_K_LINE_RESOLUTIONS = (15, 60, 300, 900, 3600, 14400, 86400) 6 | PRIVATE_ENDPOINTS = ( 7 | "positions", 8 | "wallet", 9 | "account", 10 | "spot_margin", 11 | "srm_stakes", 12 | "orders", 13 | "conditional_orders", 14 | "leverage", 15 | "subaccounts", 16 | "fills", 17 | "funding_payments", 18 | ) 19 | VALID_CHAINS = ("omni", "erc20", "trx", "sol", "bep2") 20 | RATE_LIMIT_PER_SECOND = 30 21 | -------------------------------------------------------------------------------- /FTX/helpers.py: -------------------------------------------------------------------------------- 1 | from time import time as _time 2 | 3 | 4 | def get_current_timestamp(): 5 | return int(round(_time() * 1_000)) 6 | 7 | 8 | def build_query(**kwargs): 9 | query = {} 10 | for key, value in kwargs.items(): 11 | if value is not None: 12 | query[key] = value 13 | return query 14 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Lee Chun Hao 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FTX-Trader 2 | 3 | ## Warning 4 | 5 | This is an UNOFFICIAL wrapper for FTX exchange [HTTP API](https://docs.ftx.com/) written in Python 3.7 6 | 7 | The library can be used to fetch market data, make trades, place orders or create third-party clients 8 | 9 | USE THIS WRAPPER AT YOUR OWN RISK, I WILL NOT CORRESPOND TO ANY LOSES 10 | 11 | ## Features 12 | 13 | - Except OTC and Option, implementation of all [public](#) and [private](#) endpoints 14 | - Simple handling of [authentication](https://docs.ftx.com/#authentication) with API key and secret 15 | - For asset safety, WITHDRAWAL function will never be supported ! 16 | 17 | ## Donate 18 | 19 | If useful, buy me a coffee? 20 | 21 | - ETH: 0x00000000051CBcE3fD04148CcE2c0adc7c651829 (brendanc.eth) 22 | 23 | ## Installation 24 | 25 | $ git clone https://github.com/LeeChunHao2000/ftx-api-wrapper-python3 26 | 27 | - This wrapper requires [requests](https://github.com/psf/requests) 28 | 29 | ## Requirement 30 | 31 | 1. [Register an account](https://ftx.com/#a=2500518) with FTX exchange _(referral link)_ 32 | 2. [Generate API key and secret](https://ftx.com/profile), assign relevant permissions to it 33 | 3. Clone this repository, and put in the key and secret 34 | 4. Write your own trading policies 35 | 36 | ## Quickstart 37 | 38 | This is an introduction on how to get started with FTX client. First, make sure the FTX library is installed. 39 | 40 | The next thing you need to do is import the library and get an instance of the client: 41 | 42 | from FTX.client import Client 43 | client = Client('PUY_MY_API_KEY_HERE', 'PUY_MY_API_SECRET_HERE') 44 | 45 | ### Get ordedrbook 46 | 47 | >>> from FTX.client import Client 48 | >>> client = Client('PUY_MY_API_KEY_HERE', 'PUY_MY_API_SECRET_HERE') 49 | >>> result = client.get_public_orderbook('BTC/USD', 1) 50 | >>> result 51 | {'asks': [[10854.5, 11.856]], 'bids': [[10854.0, 0.4315]]} 52 | >>> result['asks'] 53 | [[10854.5, 11.856]] 54 | >>> result['bids'] 55 | [[10854.0, 0.4315]] 56 | 57 | ### Positions (DataFrame) 58 | 59 | >>> import pandas as pd 60 | >>> result = pd.DataFrame(client.get_private_account_positions()) 61 | >>> result = result.sort_values(by=['realizedPnl'], ascending=False) 62 | >>> result 63 | collateralUsed cost entryPrice ... side size unrealizedPnl 64 | 0 0.00000 0.000 NaN ... buy 0.00 0.0 65 | 49 0.00000 0.000 NaN ... buy 0.00 0.0 66 | 4 535.09500 2972.750 594.5500 ... buy 5.00 0.0 67 | 35 206.93750 2069.375 82.7750 ... buy 25.00 0.0 68 | 3 0.00000 0.000 NaN ... buy 0.00 0.0 69 | 5 152.28000 1522.800 2.5380 ... buy 600.00 0.0 70 | ### Version Logs 71 | #### 2020-12-24 72 | 73 | **Bugfixes** 74 | - Fixed a bug with function of cancel orders 75 | 76 | **Features** 77 | - Add Spot Margin Support 78 | #### 2020-09-14 79 | 80 | - Birth! 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.12.5 2 | chardet==4.0.0 3 | expiringdict==1.2.1 4 | idna==2.10 5 | requests==2.25.1 6 | urllib3==1.26.4 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='ftx-api-wrapper-python3', 4 | version='0.1', 5 | description='FTX Exchange API wrapper in python3', 6 | url='https://github.com/LeeChunHao2000/ftx-api-wrapper-python3', 7 | author='Brendan C. Lee', 8 | license='MIT License', 9 | packages=['FTX'], 10 | ) 11 | --------------------------------------------------------------------------------