├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .mergify.yml ├── LICENSE ├── README.md ├── codecov.yml ├── docker ├── Dockerfile ├── docker-compose.yml └── entrypoint.sh ├── docs ├── changelog.md └── endpoints.md ├── examples ├── async_get_balances.py ├── futures.py ├── get_account_balances.py ├── get_order_history.py ├── get_quote.py └── place_order.py ├── nexo ├── __init__.py ├── async_client.py ├── base_client.py ├── client.py ├── exceptions.py ├── helpers.py └── response_serializers.py ├── requirements.txt ├── setup.py └── tests ├── test_api_request.py ├── test_helpers.py └── test_serialized_responses.py /.env.example: -------------------------------------------------------------------------------- 1 | NEXO_PUBLIC_KEY= 2 | NEXO_SECRET_KEY= 3 | TESTING=false 4 | LOG_LEVEL=DEBUG -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: 'guilyx,sampreets3' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: 'guilyx' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest] 11 | python-version: [3.6, 3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 3.7 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: 3.x 19 | - uses: actions/checkout@v3 20 | - name: Install Python dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | - name: Lint with flake8 24 | run: | 25 | python -m pip install flake8 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Checkout reposistory 31 | uses: actions/checkout@master 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest] 11 | python-version: [3.6, 3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: 3.x 19 | - uses: actions/checkout@v3 20 | - name: Install Python dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install -r requirements.txt 24 | - name: Checkout repository 25 | uses: actions/checkout@master 26 | - name: Test with pytest 27 | run: | 28 | pip install pytest 29 | pip install pytest-cov 30 | pytest --cov=./ 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v3 33 | with: 34 | name: codecov-umbrella -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .env 3 | build/* 4 | dist/* 5 | *.egg-info -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Assign the main reviewers 3 | conditions: 4 | - check-success=CodeFactor 5 | actions: 6 | request_reviews: 7 | users: 8 | - guilyx 9 | - name: Delete head branch after merge 10 | conditions: 11 | - merged 12 | actions: 13 | delete_head_branch: {} 14 | - name: Ask to resolve conflict 15 | conditions: 16 | - conflict 17 | actions: 18 | comment: 19 | message: This pull request is now in conflicts. Could you fix it @{{author}}? 🙏 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Erwin Lejeune 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Unofficial Nexo API Wrapper (Python) 4 | 5 | ✨ A Python wrapper for the Nexo Pro API ✨ 6 | 7 |
8 | 9 |
10 | 11 | ![lint](https://github.com/guilyx/python-nexo/workflows/lint/badge.svg?branch=master) 12 | [![tests](https://github.com/guilyx/python-nexo/actions/workflows/tests.yml/badge.svg)](https://github.com/guilyx/python-nexo/actions/workflows/tests.yml) 13 | [![codecov](https://codecov.io/gh/guilyx/python-nexo/branch/master/graph/badge.svg?token=GXUOT9P1WE)](https://codecov.io/gh/guilyx/python-nexo) 14 | [![CodeFactor](https://www.codefactor.io/repository/github/guilyx/python-nexo/badge)](https://www.codefactor.io/repository/github/guilyx/python-nexo) 15 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/guilyx/python-nexo.svg)](http://isitmaintained.com/project/guilyx/python-nexo "Percentage of issues still open") 16 | ![PipPerMonths](https://img.shields.io/pypi/dm/python-nexo.svg) 17 | [![Pip version fury.io](https://badge.fury.io/py/python-nexo.svg)](https://pypi.python.org/pypi/python-nexo/) 18 | [![GitHub license](https://img.shields.io/github/license/guilyx/python-nexo.svg)](https://github.com/guilyx/python-nexo/blob/master/LICENSE) 19 | [![GitHub contributors](https://img.shields.io/github/contributors/guilyx/python-nexo.svg)](https://GitHub.com/guilyx/python-nexo/graphs/contributors/) 20 | 21 |
22 | 23 |
24 | 25 | [Report Bug](https://github.com/guilyx/python-nexo/issues) · [Request Feature](https://github.com/guilyx/python-nexo/issues) 26 | 27 |
28 | 29 | ## About Nexo 💸 30 | 31 | Nexo is a crypto lending platform that lets you borrow crypto or cash by placing your crypto as collateral. They offer high APY on holdings based on loyalty tier (the more Nexo token you hold the higher your tier). You can earn your interests in the same kind of your holding or as Nexo tokens. As an example, stablecoins can earn up to 12% APY. Bitcoin and Ethereum, 8%. 32 | 33 | Unfortunately, Nexo doesn't offer some automated way of buying periodically. All you can do is setup a bank transfer and then convert/buy manually. This API Wrapper aims to offer a way of automating your purchases. You'd just have to setup your periodic bank transfer to Nexo, and then buy at spot price the coins that you want in an automated way by using the wrapped API calls. 34 | 35 | ## Description 📰 36 | 37 | This is an unofficial Python wrapper for the Nexo Pro exchange REST API v1. I am in no way affiliated with Nexo, use at your own risk. 38 | 39 | If you came here looking for the Nexo exchange to purchase cryptocurrencies, then go to the official Nexo website. If you want to automate interactions with Nexo, stick around. 40 | 41 | [Click here to register a Nexo account](https://nexo.io/ref/vaqo55u5py?src=web-link) 42 | 43 | Heavily influenced by [python-binance](https://github.com/sammchardy/python-binance) 44 | 45 | You can check which endpoints are currently functional [here](https://github.com/guilyx/python-nexo/blob/master/docs/endpoints.md) 46 | 47 | - ✨ Work in Progress 48 | - 🎌 Built with Python 49 | - 🐋 Docker Available 50 | - 🍻 Actively Maintained 51 | 52 | ## Roadmap 🌱 53 | 54 | See it on Issue https://github.com/guilyx/python-nexo/issues/2 55 | Checkout the [Changelog](https://github.com/guilyx/python-nexo/blob/master/docs/changelog.md) 56 | 57 | ## Preparation 🔎 58 | 59 | - Register a Nexo Account. [here](https://nexo.io/ref/vaqo55u5py?src=web-link) 60 | - Generate an API Key in Nexo Pro with the permissions you want. 61 | 62 | ## Advice 63 | 64 | Priviledge Async Client. The advantage of async processing is that we don’t need to block on I/O which is every action that we make when we interact with the Nexo Pro servers. 65 | 66 | By not blocking execution we can continue processing data while we wait for responses or new data from websockets. 67 | 68 | ## Set it up 💾 69 | 70 | ### PIP 71 | 72 | 1. Install the pip package: `python3 -m pip install python-nexo` 73 | 2. Explore the API: 74 | 75 | ```python3 76 | #### Sync 77 | 78 | import nexo 79 | import os 80 | from dotenv import load_dotenv 81 | 82 | # Loads your API keys from the .env file you created 83 | load_dotenv() 84 | key = os.getenv("NEXO_PUBLIC_KEY") 85 | secret = os.getenv("NEXO_SECRET_KEY") 86 | 87 | # Instantiate Client and grab account balances 88 | c = nexo.Client(key, secret) 89 | balances = c.get_account_balances() 90 | print(balances) 91 | 92 | #### Async 93 | 94 | import nexo 95 | import os 96 | import asyncio 97 | from dotenv import load_dotenv 98 | 99 | load_dotenv() 100 | 101 | key = os.getenv("NEXO_PUBLIC_KEY") 102 | secret = os.getenv("NEXO_SECRET_KEY") 103 | 104 | async def main(): 105 | client = await nexo.AsyncClient.create(key, secret) 106 | print(await client.get_account_balances()) 107 | 108 | await client.close_connection() 109 | 110 | if __name__ == "__main__": 111 | loop = asyncio.get_event_loop() 112 | loop.run_until_complete(main()) 113 | ``` 114 | 115 | ### Docker (source) 116 | 117 | 1. Clone the Project: `git clone -b master https://github.com/guilyx/python-nexo.git` 118 | 2. Move to the Repository: `cd python-nexo` 119 | 3. Create a copy of `.env.example` and name it `.env` 120 | 4. Fill up your API Key/Secret 121 | 5. Build and Compose the Docker: `docker-compose -f docker/docker-compose.yml up` - The container should keep running so that you can explore the API 122 | 6. Attach to the docker: `docker exec -it $(docker ps -qf "name=docker_python-nexo") /bin/bash` 123 | 7. Run python in the docker's bash environment: `python3` 124 | 8. From there, copy the following snippet to instantiate a Client: 125 | 126 | ```python3 127 | import nexo 128 | import os 129 | nexo_key = os.getenv("NEXO_PUBLIC_KEY") 130 | nexo_secret = os.getenv("NEXO_SECRET_KEY") 131 | assert(nexo_key) 132 | assert(nexo_secret) 133 | c = nexo.Client(nexo_key, nexo_secret) 134 | ``` 135 | 136 | 9. You can now explore the client's exposed endpoints, for instance: 137 | 138 | ```python3 139 | balances = c.get_account_balances() 140 | print(balances) 141 | ``` 142 | 143 | ## Contribute 🆘 144 | 145 | Open an issue to state clearly the contribution you want to make. Upon aproval send in a PR with the Issue referenced. (Implement Issue #No / Fix Issue #No). 146 | 147 | ## Maintainers Ⓜ️ 148 | 149 | - Erwin Lejeune 150 | 151 | ## Buy me a Coffee 152 | 153 | *ERC-20 / EVM: **0x07ed706146545d01fa66a3c08ebca8c93a0089e5*** 154 | 155 | *BTC: **bc1q3lu85cfkrc20ut64v90y428l79wfnv83mu72jv*** 156 | 157 | *DOT: **1Nt7G2igCuvYrfuD2Y3mCkFaU4iLS9AZytyVgZ5VBUKktjX*** 158 | 159 | *DAG: **DAG7rGLbD71VrU6nWPrepdzcyRS6rFVvfWjwRKg5*** 160 | 161 | *LUNC: **terra12n3xscq5efr7mfd6pk5ehtlsgmaazlezhypa7g*** 162 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | ignore: 18 | - "setup.py" 19 | - "examples/*" 20 | - "nexo/async_client.py" 21 | - "nexo/client.py" 22 | 23 | comment: 24 | layout: "reach,diff,flags,files,footer" 25 | behavior: default 26 | require_changes: no 27 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV TZ Europe/Paris 4 | 5 | LABEL maintainer="Erwin Lejeune " 6 | 7 | RUN apt-get update -y && \ 8 | apt-get install -y --no-install-recommends python3 && \ 9 | apt-get install -y --no-install-recommends python3-pip && \ 10 | apt-get install -y --no-install-recommends wget && \ 11 | apt-get install -y --no-install-recommends git && \ 12 | apt-get install -y --no-install-recommends python3-pytest && \ 13 | python3 -m pip install --no-cache-dir --upgrade pip && \ 14 | apt-get install -y --no-install-recommends python3-dotenv && \ 15 | apt-get clean && rm -rf /var/lib/apt/lists/* 16 | 17 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 18 | 19 | RUN git clone -b master https://github.com/guilyx/python-nexo.git 20 | 21 | WORKDIR /python-nexo 22 | 23 | RUN python3 -m pip install --no-cache-dir -r requirements.txt 24 | 25 | COPY docker/entrypoint.sh /. 26 | 27 | RUN chmod +x /entrypoint.sh 28 | 29 | EXPOSE 8080 30 | 31 | ENTRYPOINT [ "/entrypoint.sh" ] -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | python-nexo: 4 | build: 5 | context: .. 6 | dockerfile: ./docker/Dockerfile 7 | volumes: 8 | - ../:/python-nexo 9 | ports: 10 | - 8080 11 | environment: 12 | - ENV=dev 13 | - TESTING 14 | - LOG_LEVEL 15 | - NEXO_PUBLIC_KEY 16 | - NEXO_SECRET_KEY 17 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source .env 5 | 6 | if [ ${TESTING} = "true" ] 7 | then 8 | pytest-3 9 | else 10 | tail -f /dev/null 11 | fi 12 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.2] - 04/12/2022 8 | ### Changed 9 | - Readme Examples 10 | 11 | ## [1.0.2] - 04/12/2022 12 | ### Added 13 | - Asynchronous Client 14 | - New Endpoints 15 | 16 | ### Removed 17 | - Code Coverage for upper Clients classes 18 | 19 | ## [1.0.1] - 05-10-2022 20 | ### Added 21 | - Example for placing order 22 | - More Unit Tests 23 | ### Changed 24 | - POST Requests are functional 25 | - Sleep in test to avoid blowing up the rate limit 26 | - Improve error management and exceptions 27 | 28 | ## [1.0.0] - 03-10-2022 29 | ### Added 30 | - This CHANGELOG file to hopefully serve as an evolving example of a 31 | standardized open source project CHANGELOG. 32 | - Class Objects for API Responses, serialized from JSON. 33 | - Endpoints documentation 34 | 35 | ### Changed 36 | - HMAC Signature Generation has been fixed 37 | 38 | ### Removed 39 | - main.py is now removed. To try out the library, users will now use docker-compose, attach to it, run python, import the project and run some methods. 40 | 41 | ## [0.0.1] - 02-10-2022 42 | ### Added 43 | - Basic API Client with faulty HMAC Signature Generation 44 | - All Endpoints wrapped 45 | - Readme with examples, setup steps 46 | - Custom Exceptions based on Nexo's API 47 | - Basic Tests 48 | -------------------------------------------------------------------------------- /docs/endpoints.md: -------------------------------------------------------------------------------- 1 | > :warning: **Disclaimer**: 2 | 3 | > * API Documentation has yet to come 4 | 5 | ## [Nexo Pro API](https://pro.nexo.io/api-doc-pro) 6 | 7 | ### Account 8 | 9 | * **GET** /api/v1/accountSummary (Retrieves account balances) ✔️ 10 | 11 | ```python3 12 | client.get_account_balances() 13 | ``` 14 | 15 | ### Pairs 16 | 17 | * **GET** /api/v1/pairs (Gets a list of all pairs, min and max amounts.) ✔️ 18 | 19 | ```python3 20 | client.get_pairs() 21 | ``` 22 | 23 | ### Quote 24 | 25 | * **GET** /api/v1/pairs (Gets a price quote.) ✔️ 26 | 27 | ```python3 28 | client.get_price_quote(pair="BTC/ETH", amount="1.0", side="buy") 29 | ``` 30 | 31 | ### Order 32 | 33 | * **POST** /api/v1/orders (Places an order.) ✔️ 34 | 35 | ```python3 36 | client.place_order(pair="BTC/ETH", quantity="1.0", side="buy") 37 | ``` 38 | 39 | * **POST** /api/v1/orders/cancel (Cancels an order.) ✔️ 40 | 41 | ```python3 42 | def cancel_order(self, "8037298b-3ba4-41f9-8718-8a7bf87560f6") 43 | ``` 44 | 45 | * **POST** /api/v1/orders/cancel/all (Cancels all orders for a pair) ✔️ 46 | 47 | ```python3 48 | def cancel_all_orders(self, "ETH/USDT") 49 | ``` 50 | 51 | * **POST** /api/v1/orders/trigger (Places a trigger order.) ✔️ 52 | 53 | ```python3 54 | client.place_trigger_order(pair="BTC/ETH", trigger_type="takeProfit", side="buy", trigger_price="15.0", amount="2.0") 55 | ``` 56 | 57 | * **POST** /api/v1/orders/advanced (Places an advanced order.) ✔️ 58 | 59 | ```python3 60 | client.place_advanced_order(pair="BTC/USDT", side="buy", stop_loss_price="18000", tak_profit_price="22000", amount="0.001") 61 | ``` 62 | 63 | * **POST** /api/v1/orders/twap (Places a TWAP order.) ❌ 64 | 65 | ```python3 66 | client.place_twap_order(pair="BTC/USDT", side="buy", execution_interval="10", splits="100", quantity="0.001") 67 | ``` 68 | 69 | ### History 70 | 71 | * **GET** /api/v1/orders (Gets order history.) ✔️ 72 | 73 | ```python3 74 | client.get_order_history(pairs=["BTC/ETH", "BTC/USDT"], start_date="1232424242424", end_date="131415535356", page_size="30", page_num="3") 75 | ``` 76 | 77 | * **GET** /api/v1/orderDetails (Gets details of specific order.) ✔️ 78 | 79 | ```python3 80 | client.get_order_details(id="1324") 81 | ``` 82 | 83 | * **GET** /api/v1/trades (Retrieves trades history.) ✔️ 84 | 85 | ```python3 86 | client.get_trade_history(pairs=["BTC/ETH", "BTC/USDT"], start_date="1232424242424", end_date="131415535356", page_size="30", page_num="3") 87 | ``` 88 | 89 | * **GET** /api/v1/transactionInfo (Gets a transaction information.) ❌ 90 | 91 | ```python3 92 | client.get_price_quote(transaction_id="22442") 93 | ``` 94 | 95 | ### Futures 96 | 97 | * **GET** /api/v1/futures/instruments (Retrieves futures instruments) ✔️ 98 | 99 | ```python3 100 | client.get_all_future_instruments() 101 | ``` 102 | 103 | * **GET** /api/v1/futures/positions (Retrieves futures positions) ✔️ 104 | 105 | ```python3 106 | client.get_future_positions(status="any") 107 | ``` 108 | 109 | * **POST** /api/v1/futures/order (Places a futures order) ✔️ 110 | 111 | ```python3 112 | client.place_future_order() 113 | ``` 114 | 115 | * **POST** /api/v1/future/close-all-positions (Closes all future positions) ❌ 116 | 117 | ```python3 118 | client.close_all_future_positions() 119 | ``` 120 | -------------------------------------------------------------------------------- /examples/async_get_balances.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | import nexo 7 | import os 8 | import asyncio 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | key = os.getenv("NEXO_PUBLIC_KEY") 14 | secret = os.getenv("NEXO_SECRET_KEY") 15 | 16 | async def main(): 17 | client = await nexo.AsyncClient.create(key, secret) 18 | print(await client.get_account_balances()) 19 | 20 | await client.close_connection() 21 | 22 | if __name__ == "__main__": 23 | loop = asyncio.get_event_loop() 24 | loop.run_until_complete(main()) 25 | -------------------------------------------------------------------------------- /examples/futures.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | import time 4 | 5 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 6 | 7 | import nexo 8 | import os 9 | from dotenv import load_dotenv 10 | 11 | import time 12 | 13 | load_dotenv() 14 | 15 | key = os.getenv("NEXO_PUBLIC_KEY") 16 | secret = os.getenv("NEXO_SECRET_KEY") 17 | 18 | client = nexo.Client(key, secret) 19 | instruments = client.get_all_future_instruments() 20 | print(instruments) 21 | 22 | positions = client.get_future_positions(status="any") 23 | print(positions) 24 | 25 | print(client.close_all_future_positions()) 26 | -------------------------------------------------------------------------------- /examples/get_account_balances.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | import nexo 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | import time 11 | 12 | load_dotenv() 13 | 14 | key = os.getenv("NEXO_PUBLIC_KEY") 15 | secret = os.getenv("NEXO_SECRET_KEY") 16 | 17 | client = nexo.Client(key, secret) 18 | balances = client.get_account_balances() 19 | print(balances) 20 | -------------------------------------------------------------------------------- /examples/get_order_history.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | import time 4 | 5 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 6 | 7 | import nexo 8 | import os 9 | from dotenv import load_dotenv 10 | 11 | import time 12 | 13 | load_dotenv() 14 | 15 | key = os.getenv("NEXO_PUBLIC_KEY") 16 | secret = os.getenv("NEXO_SECRET_KEY") 17 | 18 | client = nexo.Client(key, secret) 19 | hist = client.get_order_history(["ATOM/USDT", "ETH/USDT"], 0, None, None, None) 20 | print(hist) 21 | -------------------------------------------------------------------------------- /examples/get_quote.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | import nexo 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | 11 | load_dotenv() 12 | 13 | key = os.getenv("NEXO_PUBLIC_KEY") 14 | secret = os.getenv("NEXO_SECRET_KEY") 15 | 16 | client = nexo.Client(key, secret) 17 | 18 | # Buys 0.03 ETH with USDT at market price 19 | quote = client.get_price_quote("ETH/USDT", "100.0", "buy") 20 | print(quote) 21 | -------------------------------------------------------------------------------- /examples/place_order.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | import nexo 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | 11 | load_dotenv() 12 | 13 | key = os.getenv("NEXO_PUBLIC_KEY") 14 | secret = os.getenv("NEXO_SECRET_KEY") 15 | 16 | client = nexo.Client(key, secret) 17 | 18 | # Buys 0.03 ETH with USDT at market price 19 | order_resp = client.place_order( 20 | "ETH/USDT", "buy", "market", "0.03", serialize_json_to_object=True 21 | ) 22 | print(order_resp) 23 | 24 | # Gets order details 25 | order = client.get_order_details(str(order_resp.order_id)) 26 | print(order) 27 | 28 | # Sells 0.03 ETH for USDT at limit price 2000 USDT 29 | order_resp = client.place_order( 30 | "ETH/USDT", "sell", "limit", "0.03", "1500", serialize_json_to_object=True 31 | ) 32 | print(order_resp) 33 | 34 | cancel_order = client.cancel_all_orders("ETH/USDT") 35 | print(cancel_order) 36 | 37 | print(client.cancel_order("kerar")) 38 | -------------------------------------------------------------------------------- /nexo/__init__.py: -------------------------------------------------------------------------------- 1 | """An unofficial Python wrapper for the Nexo Pro exchange API v1 2 | .. moduleauthor:: Erwin Lejeune 3 | """ 4 | 5 | __version__ = '1.0.3' 6 | 7 | from nexo.async_client import AsyncClient 8 | from nexo.client import Client 9 | from nexo.response_serializers import ( 10 | AdvancedOrderResponse, 11 | Balances, 12 | Orders, 13 | Pairs, 14 | Quote, 15 | TradeHistory, 16 | Transaction, 17 | OrderResponse, 18 | OrderDetails, 19 | ) 20 | from nexo.exceptions import ( 21 | NexoAPIException, 22 | NotImplementedException, 23 | NexoRequestException, 24 | NEXO_API_ERROR_CODES, 25 | ) 26 | -------------------------------------------------------------------------------- /nexo/async_client.py: -------------------------------------------------------------------------------- 1 | from nexo.base_client import BaseClient 2 | from typing import Dict, Optional, List, Tuple 3 | 4 | from operator import itemgetter 5 | import aiohttp 6 | import asyncio 7 | import json 8 | import time 9 | 10 | from nexo.exceptions import NexoAPIException, NEXO_API_ERROR_CODES, NexoRequestException 11 | from nexo.helpers import check_pair_validity, compact_json_dict 12 | from nexo.response_serializers import ( 13 | Balances, 14 | AdvancedOrderResponse, 15 | OrderDetails, 16 | OrderResponse, 17 | Orders, 18 | Pairs, 19 | Transaction, 20 | Quote, 21 | TradeHistory, 22 | ) 23 | 24 | 25 | class AsyncClient(BaseClient): 26 | def __init__(self, api_key, api_secret, loop=None): 27 | self.loop = loop or asyncio.get_event_loop() 28 | super().__init__(api_key, api_secret) 29 | self._init_session() 30 | 31 | @classmethod 32 | async def create(cls, api_key=None, api_secret=None, loop=None): 33 | return cls(api_key, api_secret, loop) 34 | 35 | def _init_session(self): 36 | session = aiohttp.ClientSession(loop=self.loop) 37 | headers = { 38 | "Accept": "application/json", 39 | "User-Agent": "python-nexo", 40 | "Content-Type": "application/json", 41 | "X-API-KEY": self.API_KEY, 42 | } 43 | session.headers.update(headers) 44 | 45 | self.session = session 46 | 47 | async def close_connection(self): 48 | if self.session: 49 | assert self.session 50 | await self.session.close() 51 | 52 | async def _handle_response(self, response: aiohttp.ClientResponse): 53 | json_response = {} 54 | 55 | try: 56 | json_response = await response.json() 57 | except Exception: 58 | if not response.ok: 59 | raise NexoRequestException( 60 | f"Failed to get API response: \nCode: {response.status_code}\nRequest: {str(response.request.body)}" 61 | ) 62 | 63 | try: 64 | if "errorCode" in json_response: 65 | if json_response["errorCode"] in NEXO_API_ERROR_CODES: 66 | raise NexoAPIException( 67 | json_response["errorCode"], await response.text() 68 | ) 69 | else: 70 | raise NexoRequestException( 71 | f'Invalid Response: status: {json_response["errorCode"]}, message: {json_response["errorMessage"]}\n body: {response.request.body}' 72 | ) 73 | else: 74 | if not response.ok: 75 | raise NexoRequestException( 76 | f"Failed to get API response: \nCode: {response.status_code}\nRequest: {str(response.request.body)}" 77 | ) 78 | 79 | return json_response 80 | 81 | except ValueError: 82 | raise NexoRequestException("Invalid Response: %s" % json_response) 83 | 84 | async def _request( 85 | self, method, path: str, version=BaseClient.PUBLIC_API_VERSION, **kwargs 86 | ): 87 | # set default requests timeout 88 | kwargs["timeout"] = 10 89 | 90 | kwargs["data"] = kwargs.get("data", {}) 91 | kwargs["headers"] = kwargs.get("headers", {}) 92 | 93 | full_path = self._create_path(path, version) 94 | uri = self._create_api_uri(full_path) 95 | 96 | nonce = str(int(time.time() * 1000)) 97 | kwargs["headers"]["X-NONCE"] = nonce 98 | kwargs["headers"]["X-SIGNATURE"] = self._generate_signature(nonce).decode( 99 | "utf8" 100 | ) 101 | 102 | if kwargs["data"] and method == "get": 103 | kwargs["params"] = kwargs["data"] 104 | del kwargs["data"] 105 | 106 | if method != "get" and kwargs["data"]: 107 | kwargs["data"] = compact_json_dict(kwargs["data"]) 108 | 109 | async with getattr(self.session, method)(uri, **kwargs) as response: 110 | self.response = response 111 | return await self._handle_response(response) 112 | 113 | async def _get(self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs): 114 | return await self._request("get", path, version, **kwargs) 115 | 116 | async def _post( 117 | self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs 118 | ) -> Dict: 119 | return await self._request("post", path, version, **kwargs) 120 | 121 | async def _put(self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs) -> Dict: 122 | return await self._request("put", path, version, **kwargs) 123 | 124 | async def _delete( 125 | self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs 126 | ) -> Dict: 127 | return await self._request("delete", path, version, **kwargs) 128 | 129 | async def get_account_balances( 130 | self, serialize_json_to_object: bool = False 131 | ) -> Dict: 132 | balances_json = await self._get("accountSummary") 133 | 134 | if serialize_json_to_object: 135 | return Balances(balances_json) 136 | 137 | return balances_json 138 | 139 | async def get_pairs(self, serialize_json_to_object: bool = False) -> Dict: 140 | pairs_json = await self._get("pairs") 141 | 142 | if serialize_json_to_object: 143 | return Pairs(pairs_json) 144 | 145 | return pairs_json 146 | 147 | async def get_price_quote( 148 | self, 149 | pair: str, 150 | amount: float, 151 | side: str, 152 | exchanges: str = None, 153 | serialize_json_to_object: bool = False, 154 | ) -> Dict: 155 | if side != "buy" and side != "sell": 156 | raise NexoRequestException( 157 | f"Bad Request: Tried to get price quote with side = {side}, side must be 'buy' or 'sell'" 158 | ) 159 | if not check_pair_validity(pair): 160 | raise NexoRequestException( 161 | f"Bad Request: Tried to place a trigger order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 162 | ) 163 | 164 | data = {"side": side, "amount": amount, "pair": pair} 165 | 166 | if exchanges: 167 | data["exchanges"] = exchanges 168 | 169 | quote_json = await self._get("quote", data=data) 170 | 171 | if serialize_json_to_object: 172 | return Quote(quote_json) 173 | 174 | return quote_json 175 | 176 | async def get_order_history( 177 | self, 178 | pairs: List[str], 179 | start_date: int, 180 | end_date: int, 181 | page_size: int, 182 | page_num: int, 183 | serialize_json_to_object: bool = False, 184 | ) -> Dict: 185 | data = { 186 | "pairs": pairs, 187 | "startDate": start_date, 188 | "endDate": end_date, 189 | "pageSize": page_size, 190 | "pageNum": page_num, 191 | } 192 | orders_json = await self._get("orders", data=data) 193 | 194 | if serialize_json_to_object: 195 | return Orders(orders_json) 196 | 197 | return orders_json 198 | 199 | async def get_order_details( 200 | self, id: str, serialize_json_to_object: bool = False 201 | ) -> Dict: 202 | data = { 203 | "id": id, 204 | } 205 | 206 | order_details_json = await self._get(f"orderDetails", data=data) 207 | 208 | if serialize_json_to_object: 209 | return OrderDetails(order_details_json) 210 | 211 | return order_details_json 212 | 213 | async def get_trade_history( 214 | self, 215 | pairs: List[str], 216 | start_date: int, 217 | end_date: int, 218 | page_size: int, 219 | page_num: int, 220 | serialize_json_to_object: bool = False, 221 | ) -> Dict: 222 | for pair in pairs: 223 | if not check_pair_validity(pair): 224 | raise NexoRequestException( 225 | f"Bad Request: Tried to get trade history with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 226 | ) 227 | 228 | data = { 229 | "pairs": pairs, 230 | "startDate": start_date, 231 | "endDate": end_date, 232 | "pageSize": page_size, 233 | "pageNum": page_num, 234 | } 235 | 236 | trades_json = await self._get("trades", data=data) 237 | 238 | if serialize_json_to_object: 239 | return TradeHistory(trades_json) 240 | 241 | return trades_json 242 | 243 | async def get_transaction_info( 244 | self, transaction_id: str, serialize_json_to_object: bool = False 245 | ) -> Dict: 246 | 247 | data = {"transactionId": transaction_id} 248 | 249 | transaction_json = await self._get(f"transaction", data=data) 250 | 251 | if serialize_json_to_object: 252 | return Transaction(transaction_json) 253 | 254 | return transaction_json 255 | 256 | async def place_order( 257 | self, 258 | pair: str, 259 | side: str, 260 | type: str, 261 | quantity: float, 262 | price: float = None, 263 | serialize_json_to_object: bool = False, 264 | ) -> Dict: 265 | if side != "buy" and side != "sell": 266 | raise NexoRequestException( 267 | f"Bad Request: Tried to place an order with side = {side}, side must be 'buy' or 'sell'" 268 | ) 269 | if type != "market" and type != "limit": 270 | raise NexoRequestException( 271 | f"Bad Request: Tried to place an order with type = {type}, side must be 'market' or 'limit'" 272 | ) 273 | if not check_pair_validity(pair): 274 | raise NexoRequestException( 275 | f"Bad Request: Tried to place an order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 276 | ) 277 | 278 | data = {"pair": pair, "side": side, "type": type, "quantity": quantity} 279 | 280 | if price: 281 | data["price"] = price 282 | 283 | order_id_json = await self._post("orders", data=data) 284 | 285 | if serialize_json_to_object: 286 | return OrderResponse(order_id_json) 287 | 288 | return order_id_json 289 | 290 | async def place_trigger_order( 291 | self, 292 | pair: str, 293 | side: str, 294 | trigger_type: str, 295 | amount: float, 296 | trigger_price: float, 297 | trailing_distance: float = None, 298 | trailing_percentage: float = None, 299 | serialize_json_to_object: bool = False, 300 | ) -> Dict: 301 | if side != "buy" and side != "sell": 302 | raise NexoRequestException( 303 | f"Bad Request: Tried to place a trigger order with side = {side}, side must be 'buy' or 'sell'" 304 | ) 305 | if ( 306 | trigger_type != "stopLoss" 307 | and trigger_type != "takeProfit" 308 | and trigger_type != "trailing" 309 | ): 310 | raise NexoRequestException( 311 | f"Bad Request: Tried to place a trigger order with trigger type = {trigger_type}, trigger type must be 'stopLoss' or 'takeProfit' or 'trailing'" 312 | ) 313 | if not check_pair_validity(pair): 314 | raise NexoRequestException( 315 | f"Bad Request: Tried to place a trigger order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 316 | ) 317 | 318 | data = { 319 | "pair": pair, 320 | "side": side, 321 | "triggerType": trigger_type, 322 | "amount": amount, 323 | "triggerPrice": trigger_price, 324 | } 325 | 326 | if trailing_distance: 327 | data["trailingDistance"] = trailing_distance 328 | 329 | if trailing_percentage: 330 | data["trailingPercentage"] = trailing_percentage 331 | 332 | order_id_json = await self._post("orders", data=data) 333 | 334 | if serialize_json_to_object: 335 | return OrderResponse(order_id_json) 336 | 337 | return order_id_json 338 | 339 | async def place_advanced_order( 340 | self, 341 | pair: str, 342 | side: str, 343 | amount: str, 344 | stop_loss_price: str, 345 | take_profit_price: str, 346 | serialize_json_to_object: bool = False, 347 | ) -> Dict: 348 | if side != "buy" and side != "sell": 349 | raise NexoRequestException( 350 | f"Bad Request: Tried to place an advanced order with side = {side}, side must be 'buy' or 'sell'" 351 | ) 352 | 353 | if not check_pair_validity(pair): 354 | raise NexoRequestException( 355 | f"Bad Request: Tried to place an advanced order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 356 | ) 357 | 358 | data = { 359 | "pair": pair, 360 | "side": side, 361 | "amount": amount, 362 | "stopLossPrice": stop_loss_price, 363 | "takeProfitPrice": take_profit_price, 364 | } 365 | order_id_json = await self._post("orders", data=data) 366 | 367 | if serialize_json_to_object: 368 | return AdvancedOrderResponse(order_id_json) 369 | 370 | return order_id_json 371 | 372 | async def place_twap_order( 373 | self, 374 | pair: str, 375 | side: str, 376 | quantity: float, 377 | splits: int, 378 | execution_interval: int, 379 | exchanges: List[str] = None, 380 | serialize_json_to_object: bool = False, 381 | ) -> Dict: 382 | if side != "buy" and side != "sell": 383 | raise NexoRequestException( 384 | f"Bad Request: Tried to place a twap order with side = {side}, side must be 'buy' or 'sell'" 385 | ) 386 | 387 | if not check_pair_validity(pair): 388 | raise NexoRequestException( 389 | f"Bad Request: Tried to place a twap order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 390 | ) 391 | 392 | data = { 393 | "pair": pair, 394 | "side": side, 395 | "quantity": quantity, 396 | "splits": splits, 397 | "executionInterval": execution_interval, 398 | } 399 | 400 | if exchanges: 401 | data["exchanges"] = exchanges 402 | 403 | twap_order_json = await self._post("orders/twap", data=data) 404 | 405 | if serialize_json_to_object: 406 | return AdvancedOrderResponse(twap_order_json) 407 | 408 | return twap_order_json 409 | 410 | async def cancel_order(self, order_id: str): 411 | data = {"orderId": order_id} 412 | 413 | return await self._post("orders/cancel", data=data) 414 | 415 | async def cancel_all_orders(self, pair: str): 416 | if not check_pair_validity(pair): 417 | raise NexoRequestException( 418 | f"Bad Request: Tried to cancel all orders with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 419 | ) 420 | 421 | data = {"pair": pair} 422 | 423 | return await self._post("orders/cancel/all", data=data) 424 | 425 | async def get_all_future_instruments(self): 426 | return await self._get("futures/instruments") 427 | 428 | async def get_future_positions(self, status: str): 429 | if status != "any" and status != "active" and status != "inactive": 430 | raise NexoRequestException( 431 | f"Bad Request: Tried to get future positions with status = {status}, status must be 'any', 'active' or 'inactive'" 432 | ) 433 | 434 | data = {"status": status} 435 | 436 | return await self._get("futures/positions", data=data) 437 | 438 | async def place_future_order( 439 | self, 440 | instrument: str, 441 | position_action: str, 442 | position_side: str, 443 | type: str, 444 | quantity: float, 445 | ): 446 | if position_action != "open" and position_action != "close": 447 | raise NexoRequestException( 448 | f"Bad Request: Tried to place future position with position action = {position_action}, must be 'open' or 'close'" 449 | ) 450 | 451 | if position_side != "long" and position_side != "short": 452 | raise NexoRequestException( 453 | f"Bad Request: Tried to place future position with position side = {position_side}, must be 'long' or 'short'" 454 | ) 455 | 456 | if type != "market": 457 | raise NexoRequestException( 458 | f"Bad Request: Tried to place future position with type = {type}, must be 'market'" 459 | ) 460 | 461 | data = { 462 | "positionAction": position_action, 463 | "instrument": instrument, 464 | "positionSide": position_side, 465 | "type": type, 466 | "quantity": quantity, 467 | } 468 | 469 | return await self._post("futures/order", data=data) 470 | 471 | async def close_all_future_positions(self): 472 | return await self._post("futures/close-all-positions") 473 | -------------------------------------------------------------------------------- /nexo/base_client.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from typing import Dict, Optional, List, Tuple 3 | import hmac 4 | import hashlib 5 | 6 | class BaseClient: 7 | API_URL = "https://pro-api.nexo.io" 8 | PUBLIC_API_VERSION = "v1" 9 | 10 | REQUEST_TIMEOUT = 10 11 | 12 | def __init__(self, api_key, api_secret): 13 | self.API_KEY = api_key 14 | self.API_SECRET = api_secret 15 | self.timestamp_offset = 0 16 | 17 | def _create_path(self, path: str, api_version: str = PUBLIC_API_VERSION): 18 | return f"/api/{api_version}/{path}" 19 | 20 | def _create_api_uri(self, path: str) -> str: 21 | return f"{self.API_URL}{path}" 22 | 23 | @staticmethod 24 | def _get_params_for_sig(data: Dict) -> str: 25 | return "&".join(["{}={}".format(key, data[key]) for key in data]) 26 | 27 | def _generate_signature(self, nonce: str,) -> str: 28 | m = hmac.new( 29 | self.API_SECRET.encode("utf-8"), str(nonce).encode("utf-8"), hashlib.sha256 30 | ) 31 | return base64.b64encode(m.digest()) 32 | -------------------------------------------------------------------------------- /nexo/client.py: -------------------------------------------------------------------------------- 1 | from nexo.base_client import BaseClient 2 | from typing import Dict, Optional, List, Tuple 3 | import hmac 4 | import hashlib 5 | from operator import itemgetter 6 | import base64 7 | import urllib3 8 | import requests 9 | import json 10 | import time 11 | from nexo.exceptions import NexoAPIException, NEXO_API_ERROR_CODES, NexoRequestException 12 | from nexo.helpers import check_pair_validity, compact_json_dict 13 | from nexo.response_serializers import ( 14 | Balances, 15 | AdvancedOrderResponse, 16 | OrderDetails, 17 | OrderResponse, 18 | Orders, 19 | Pairs, 20 | Transaction, 21 | Quote, 22 | TradeHistory, 23 | ) 24 | 25 | class Client(BaseClient): 26 | def __init__(self, api_key, api_secret): 27 | super().__init__(api_key, api_secret) 28 | self.session = self._init_session() 29 | 30 | def _init_session(self): 31 | 32 | session = requests.session() 33 | headers = { 34 | "Accept": "application/json", 35 | "User-Agent": "python-nexo", 36 | "Content-Type": "application/json", 37 | "X-API-KEY": self.API_KEY, 38 | } 39 | 40 | session.headers.update(headers) 41 | return session 42 | 43 | @staticmethod 44 | def _handle_response(response: requests.Response): 45 | json_response = {} 46 | 47 | try: 48 | json_response = response.json() 49 | except Exception: 50 | if not response.ok: 51 | raise NexoRequestException( 52 | f"Failed to get API response: \nCode: {response.status_code}\nRequest: {str(response.request.body)}" 53 | ) 54 | 55 | try: 56 | if "errorCode" in json_response: 57 | if json_response["errorCode"] in NEXO_API_ERROR_CODES: 58 | raise NexoAPIException(json_response["errorCode"], response.text) 59 | else: 60 | raise NexoRequestException( 61 | f'Invalid Response: status: {json_response["errorCode"]}, message: {json_response["errorMessage"]}\n body: {response.request.body}' 62 | ) 63 | else: 64 | if not response.ok: 65 | raise NexoRequestException( 66 | f"Failed to get API response: \nCode: {response.status_code}\nRequest: {str(response.request.body)}" 67 | ) 68 | 69 | return json_response 70 | 71 | except ValueError: 72 | raise NexoRequestException("Invalid Response: %s" % json_response) 73 | 74 | def _request( 75 | self, method, path: str, version=BaseClient.PUBLIC_API_VERSION, **kwargs 76 | ): 77 | # set default requests timeout 78 | kwargs["timeout"] = 10 79 | 80 | kwargs["data"] = kwargs.get("data", {}) 81 | kwargs["headers"] = kwargs.get("headers", {}) 82 | 83 | full_path = self._create_path(path, version) 84 | uri = self._create_api_uri(full_path) 85 | 86 | nonce = str(int(time.time() * 1000)) 87 | kwargs["headers"]["X-NONCE"] = nonce 88 | kwargs["headers"]["X-SIGNATURE"] = self._generate_signature(nonce) 89 | 90 | if kwargs["data"] and method == "get": 91 | kwargs["params"] = kwargs["data"] 92 | del kwargs["data"] 93 | 94 | if method != "get" and kwargs["data"]: 95 | kwargs["data"] = compact_json_dict(kwargs["data"]) 96 | 97 | response = getattr(self.session, method)(uri, **kwargs) 98 | return self._handle_response(response) 99 | 100 | def _get(self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs): 101 | return self._request("get", path, version, **kwargs) 102 | 103 | def _post(self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs) -> Dict: 104 | return self._request("post", path, version, **kwargs) 105 | 106 | def _put(self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs) -> Dict: 107 | return self._request("put", path, version, **kwargs) 108 | 109 | def _delete(self, path, version=BaseClient.PUBLIC_API_VERSION, **kwargs) -> Dict: 110 | return self._request("delete", path, version, **kwargs) 111 | 112 | def get_account_balances(self, serialize_json_to_object: bool = False) -> Dict: 113 | balances_json = self._get("accountSummary") 114 | 115 | if serialize_json_to_object: 116 | return Balances(balances_json) 117 | 118 | return balances_json 119 | 120 | def get_pairs(self, serialize_json_to_object: bool = False) -> Dict: 121 | pairs_json = self._get("pairs") 122 | 123 | if serialize_json_to_object: 124 | return Pairs(pairs_json) 125 | 126 | return pairs_json 127 | 128 | def get_price_quote( 129 | self, 130 | pair: str, 131 | amount: float, 132 | side: str, 133 | exchanges: str = None, 134 | serialize_json_to_object: bool = False, 135 | ) -> Dict: 136 | if side != "buy" and side != "sell": 137 | raise NexoRequestException( 138 | f"Bad Request: Tried to get price quote with side = {side}, side must be 'buy' or 'sell'" 139 | ) 140 | if not check_pair_validity(pair): 141 | raise NexoRequestException( 142 | f"Bad Request: Tried to place a trigger order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 143 | ) 144 | 145 | data = {"side": side, "amount": amount, "pair": pair} 146 | 147 | if exchanges: 148 | data["exchanges"] = exchanges 149 | 150 | quote_json = self._get("quote", data=data) 151 | 152 | if serialize_json_to_object: 153 | return Quote(quote_json) 154 | 155 | return quote_json 156 | 157 | def get_order_history( 158 | self, 159 | pairs: List[str], 160 | start_date: int, 161 | end_date: int, 162 | page_size: int, 163 | page_num: int, 164 | serialize_json_to_object: bool = False, 165 | ) -> Dict: 166 | data = { 167 | "pairs": pairs, 168 | "startDate": start_date, 169 | "endDate": end_date, 170 | "pageSize": page_size, 171 | "pageNum": page_num, 172 | } 173 | orders_json = self._get("orders", data=data) 174 | 175 | if serialize_json_to_object: 176 | return Orders(orders_json) 177 | 178 | return orders_json 179 | 180 | def get_order_details( 181 | self, id: str, serialize_json_to_object: bool = False 182 | ) -> Dict: 183 | data = { 184 | "id": id, 185 | } 186 | 187 | order_details_json = self._get(f"orderDetails", data=data) 188 | 189 | if serialize_json_to_object: 190 | return OrderDetails(order_details_json) 191 | 192 | return order_details_json 193 | 194 | def get_trade_history( 195 | self, 196 | pairs: List[str], 197 | start_date: int, 198 | end_date: int, 199 | page_size: int, 200 | page_num: int, 201 | serialize_json_to_object: bool = False, 202 | ) -> Dict: 203 | for pair in pairs: 204 | if not check_pair_validity(pair): 205 | raise NexoRequestException( 206 | f"Bad Request: Tried to get trade history with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 207 | ) 208 | 209 | data = { 210 | "pairs": pairs, 211 | "startDate": start_date, 212 | "endDate": end_date, 213 | "pageSize": page_size, 214 | "pageNum": page_num, 215 | } 216 | 217 | trades_json = self._get("trades", data=data) 218 | 219 | if serialize_json_to_object: 220 | return TradeHistory(trades_json) 221 | 222 | return trades_json 223 | 224 | def get_transaction_info( 225 | self, transaction_id: str, serialize_json_to_object: bool = False 226 | ) -> Dict: 227 | 228 | data = {"transactionId": transaction_id} 229 | 230 | transaction_json = self._get(f"transaction", data=data) 231 | 232 | if serialize_json_to_object: 233 | return Transaction(transaction_json) 234 | 235 | return transaction_json 236 | 237 | def place_order( 238 | self, 239 | pair: str, 240 | side: str, 241 | type: str, 242 | quantity: float, 243 | price: float = None, 244 | serialize_json_to_object: bool = False, 245 | ) -> Dict: 246 | if side != "buy" and side != "sell": 247 | raise NexoRequestException( 248 | f"Bad Request: Tried to place an order with side = {side}, side must be 'buy' or 'sell'" 249 | ) 250 | if type != "market" and type != "limit": 251 | raise NexoRequestException( 252 | f"Bad Request: Tried to place an order with type = {type}, side must be 'market' or 'limit'" 253 | ) 254 | if not check_pair_validity(pair): 255 | raise NexoRequestException( 256 | f"Bad Request: Tried to place an order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 257 | ) 258 | 259 | data = {"pair": pair, "side": side, "type": type, "quantity": quantity} 260 | 261 | if price: 262 | data["price"] = price 263 | 264 | order_id_json = self._post("orders", data=data) 265 | 266 | if serialize_json_to_object: 267 | return OrderResponse(order_id_json) 268 | 269 | return order_id_json 270 | 271 | def place_trigger_order( 272 | self, 273 | pair: str, 274 | side: str, 275 | trigger_type: str, 276 | amount: float, 277 | trigger_price: float, 278 | trailing_distance: float = None, 279 | trailing_percentage: float = None, 280 | serialize_json_to_object: bool = False, 281 | ) -> Dict: 282 | if side != "buy" and side != "sell": 283 | raise NexoRequestException( 284 | f"Bad Request: Tried to place a trigger order with side = {side}, side must be 'buy' or 'sell'" 285 | ) 286 | if ( 287 | trigger_type != "stopLoss" 288 | and trigger_type != "takeProfit" 289 | and trigger_type != "trailing" 290 | ): 291 | raise NexoRequestException( 292 | f"Bad Request: Tried to place a trigger order with trigger type = {trigger_type}, trigger type must be 'stopLoss' or 'takeProfit' or 'trailing'" 293 | ) 294 | if not check_pair_validity(pair): 295 | raise NexoRequestException( 296 | f"Bad Request: Tried to place a trigger order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 297 | ) 298 | 299 | data = { 300 | "pair": pair, 301 | "side": side, 302 | "triggerType": trigger_type, 303 | "amount": amount, 304 | "triggerPrice": trigger_price, 305 | } 306 | 307 | if trailing_distance: 308 | data["trailingDistance"] = trailing_distance 309 | 310 | if trailing_percentage: 311 | data["trailingPercentage"] = trailing_percentage 312 | 313 | order_id_json = self._post("orders", data=data) 314 | 315 | if serialize_json_to_object: 316 | return OrderResponse(order_id_json) 317 | 318 | return order_id_json 319 | 320 | def place_advanced_order( 321 | self, 322 | pair: str, 323 | side: str, 324 | amount: str, 325 | stop_loss_price: str, 326 | take_profit_price: str, 327 | serialize_json_to_object: bool = False, 328 | ) -> Dict: 329 | if side != "buy" and side != "sell": 330 | raise NexoRequestException( 331 | f"Bad Request: Tried to place an advanced order with side = {side}, side must be 'buy' or 'sell'" 332 | ) 333 | 334 | if not check_pair_validity(pair): 335 | raise NexoRequestException( 336 | f"Bad Request: Tried to place an advanced order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 337 | ) 338 | 339 | data = { 340 | "pair": pair, 341 | "side": side, 342 | "amount": amount, 343 | "stopLossPrice": stop_loss_price, 344 | "takeProfitPrice": take_profit_price, 345 | } 346 | order_id_json = self._post("orders", data=data) 347 | 348 | if serialize_json_to_object: 349 | return AdvancedOrderResponse(order_id_json) 350 | 351 | return order_id_json 352 | 353 | def place_twap_order( 354 | self, 355 | pair: str, 356 | side: str, 357 | quantity: float, 358 | splits: int, 359 | execution_interval: int, 360 | exchanges: List[str] = None, 361 | serialize_json_to_object: bool = False, 362 | ) -> Dict: 363 | if side != "buy" and side != "sell": 364 | raise NexoRequestException( 365 | f"Bad Request: Tried to place a twap order with side = {side}, side must be 'buy' or 'sell'" 366 | ) 367 | 368 | if not check_pair_validity(pair): 369 | raise NexoRequestException( 370 | f"Bad Request: Tried to place a twap order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 371 | ) 372 | 373 | data = { 374 | "pair": pair, 375 | "side": side, 376 | "quantity": quantity, 377 | "splits": splits, 378 | "executionInterval": execution_interval, 379 | } 380 | 381 | if exchanges: 382 | data["exchanges"] = exchanges 383 | 384 | twap_order_json = self._post("orders/twap", data=data) 385 | 386 | if serialize_json_to_object: 387 | return AdvancedOrderResponse(twap_order_json) 388 | 389 | return twap_order_json 390 | 391 | def cancel_order(self, order_id: str): 392 | data = {"orderId": order_id} 393 | 394 | return self._post("orders/cancel", data=data) 395 | 396 | def cancel_all_orders(self, pair: str): 397 | if not check_pair_validity(pair): 398 | raise NexoRequestException( 399 | f"Bad Request: Tried to cancel all orders with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 400 | ) 401 | 402 | data = {"pair": pair} 403 | 404 | return self._post("orders/cancel/all", data=data) 405 | 406 | def get_all_future_instruments(self): 407 | return self._get("futures/instruments") 408 | 409 | def get_future_positions(self, status: str): 410 | if status != "any" and status != "active" and status != "inactive": 411 | raise NexoRequestException( 412 | f"Bad Request: Tried to get future positions with status = {status}, status must be 'any', 'active' or 'inactive'" 413 | ) 414 | 415 | data = {"status": status} 416 | 417 | return self._get("futures/positions", data=data) 418 | 419 | def place_future_order( 420 | self, 421 | instrument: str, 422 | position_action: str, 423 | position_side: str, 424 | type: str, 425 | quantity: float, 426 | ): 427 | if position_action != "open" and position_action != "close": 428 | raise NexoRequestException( 429 | f"Bad Request: Tried to place future position with position action = {position_action}, must be 'open' or 'close'" 430 | ) 431 | 432 | if position_side != "long" and position_side != "short": 433 | raise NexoRequestException( 434 | f"Bad Request: Tried to place future position with position side = {position_side}, must be 'long' or 'short'" 435 | ) 436 | 437 | if type != "market": 438 | raise NexoRequestException( 439 | f"Bad Request: Tried to place future position with type = {type}, must be 'market'" 440 | ) 441 | 442 | data = { 443 | "positionAction": position_action, 444 | "instrument": instrument, 445 | "positionSide": position_side, 446 | "type": type, 447 | "quantity": quantity, 448 | } 449 | 450 | return self._post("futures/order", data=data) 451 | 452 | def close_all_future_positions(self): 453 | return self._post("futures/close-all-positions") 454 | 455 | 456 | -------------------------------------------------------------------------------- /nexo/exceptions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | NEXO_API_ERROR_CODES = { 4 | 100: "API-Key is malformed or invalid.", 5 | 101: "Request signature is malformed or invalid.", 6 | 102: "Some request field is malformed or missing.", 7 | 103: "Unauthorized.", 8 | 104: "Websocket method is invalid.", 9 | 105: "Websocket session is already authenticated.", 10 | 106: "Request nonce is malformed or invalid.", 11 | 203: "No results found for specified query.", 12 | 206: "Internal error.", 13 | 300: "The given exchanges are unsupported for said pair.", 14 | 301: "Rate limit exceeded.", 15 | } 16 | 17 | 18 | class NexoAPIException(Exception): 19 | def __init__(self, status_code: int, response: str): 20 | self.code = 0 21 | try: 22 | json_res = json.loads(response) 23 | except ValueError: 24 | self.message = "Invalid JSON error message from Nexo: {}".format( 25 | response.text 26 | ) 27 | else: 28 | self.code = json_res["errorCode"] 29 | self.message = json_res["errorMessage"] 30 | self.status_code = status_code 31 | self.response = response 32 | self.request = getattr(response, "request", None) 33 | 34 | def __str__(self): # pragma: no cover 35 | return f"APIError(code={self.code}): {self.message}, {NEXO_API_ERROR_CODES[self.code]}" 36 | 37 | 38 | class NexoRequestException(Exception): 39 | def __init__(self, message): 40 | self.message = message 41 | 42 | def __str__(self): 43 | return "NexoRequestException: %s" % self.message 44 | 45 | 46 | class NotImplementedException(Exception): 47 | def __init__(self, value): 48 | message = f"Not implemented: {value}" 49 | super().__init__(message) 50 | -------------------------------------------------------------------------------- /nexo/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Dict 3 | import json 4 | 5 | 6 | def check_pair_validity(pair: str) -> bool: 7 | assets = pair.split("/") 8 | 9 | if len(assets) != 2: 10 | return False 11 | 12 | for asset in assets: 13 | if not re.match(r"[A-Z]{2,6}", asset) or len(asset) > 6: 14 | return False 15 | 16 | return True 17 | 18 | 19 | def compact_json_dict(data: Dict): 20 | return json.dumps(data, separators=(",", ":"), ensure_ascii=False) 21 | -------------------------------------------------------------------------------- /nexo/response_serializers.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | 4 | class BaseSerializedResponse: 5 | def __init__(self, json_dictionary: Dict): 6 | self.json_dictionary = json_dictionary 7 | 8 | def __repr__(self): 9 | return str(self.json_dictionary) 10 | 11 | 12 | # GET /balances 13 | class WalletBalance(BaseSerializedResponse): 14 | def __init__(self, json_dictionary: Dict): 15 | super().__init__(json_dictionary) 16 | 17 | if "assetName" in json_dictionary: 18 | self.asset_name = json_dictionary["assetName"] 19 | if "totalBalance" in json_dictionary: 20 | self.total_balance = json_dictionary["totalBalance"] 21 | if "availableBalance" in json_dictionary: 22 | self.available_balance = json_dictionary["availableBalance"] 23 | if "lockedBalance" in json_dictionary: 24 | self.locked_balance = json_dictionary["lockedBalance"] 25 | if "debt" in json_dictionary: 26 | self.debt = json_dictionary["debt"] 27 | if "interest" in json_dictionary: 28 | self.interest = json_dictionary["interest"] 29 | 30 | 31 | class Balances(BaseSerializedResponse): 32 | def __init__(self, json_dictionary: Dict): 33 | super().__init__(json_dictionary) 34 | 35 | if "balances" in json_dictionary: 36 | self.balances = [ 37 | WalletBalance(balance) for balance in json_dictionary["balances"] 38 | ] 39 | 40 | 41 | # GET /pairs 42 | class Pairs(BaseSerializedResponse): 43 | def __init__(self, json_dictionary: Dict): 44 | super().__init__(json_dictionary) 45 | 46 | if "pairs" in json_dictionary: 47 | self.pairs = json_dictionary["pairs"] 48 | if "minLimits" in json_dictionary: 49 | self.min_limits = json_dictionary["minLimits"] 50 | if "maxLimits" in json_dictionary: 51 | self.max_limits = json_dictionary["maxLimits"] 52 | 53 | 54 | # GET /quote 55 | class Quote(BaseSerializedResponse): 56 | def __init__(self, json_dictionary: Dict): 57 | super().__init__(json_dictionary) 58 | 59 | if "pair" in json_dictionary: 60 | self.pair = json_dictionary["pair"] 61 | if "amount" in json_dictionary: 62 | self.amount = json_dictionary["amount"] 63 | if "price" in json_dictionary: 64 | self.price = json_dictionary["price"] 65 | if "timestamp" in json_dictionary: 66 | self.timestamp = json_dictionary["timestamp"] 67 | 68 | 69 | # POST /orders | /orders/trigger | /orders/advanced 70 | class OrderResponse(BaseSerializedResponse): 71 | def __init__(self, json_dictionary: Dict): 72 | super().__init__(json_dictionary) 73 | 74 | if "orderId" in json_dictionary: 75 | self.order_id = json_dictionary["orderId"] 76 | 77 | 78 | # POST /orders/twap 79 | class AdvancedOrderResponse(BaseSerializedResponse): 80 | def __init__(self, json_dictionary: Dict): 81 | super().__init__(json_dictionary) 82 | 83 | if "orderId" in json_dictionary: 84 | self.order_id = json_dictionary["orderId"] 85 | 86 | if "amount" in json_dictionary: 87 | self.amount = json_dictionary["amount"] 88 | 89 | 90 | class TradeForOrder(BaseSerializedResponse): 91 | def __init__(self, json_dictionary: Dict): 92 | super().__init__(json_dictionary) 93 | 94 | if "id" in json_dictionary: 95 | self.id = json_dictionary["id"] 96 | if "symbol" in json_dictionary: 97 | self.symbol = json_dictionary["symbol"] 98 | if "type" in json_dictionary: 99 | self.type = json_dictionary["type"] 100 | if "orderAmount" in json_dictionary: 101 | self.order_amount = json_dictionary["orderAmount"] 102 | if "amountFilled" in json_dictionary: 103 | self.amount_filled = json_dictionary["amountFilled"] 104 | if "executedPrice" in json_dictionary: 105 | self.executed_price = json_dictionary["executedPrice"] 106 | if "timestamp" in json_dictionary: 107 | self.timestamp = json_dictionary["timestamp"] 108 | if "status" in json_dictionary: 109 | self.status = json_dictionary["status"] 110 | 111 | 112 | # GET /orderDetails 113 | class OrderDetails(BaseSerializedResponse): 114 | def __init__(self, json_dictionary: Dict): 115 | super().__init__(json_dictionary) 116 | 117 | if "id" in json_dictionary: 118 | self.id = json_dictionary["id"] 119 | if "side" in json_dictionary: 120 | self.side = json_dictionary["side"] 121 | if "pair" in json_dictionary: 122 | self.pair = json_dictionary["pair"] 123 | if "timestamp" in json_dictionary: 124 | self.timestamp = json_dictionary["timestamp"] 125 | if "quantity" in json_dictionary: 126 | self.quantity = json_dictionary["quantity"] 127 | if "exchangeRate" in json_dictionary: 128 | self.exchange_rate = json_dictionary["exchangeRate"] 129 | if "exchangeQuantity" in json_dictionary: 130 | self.exchange_quantity = json_dictionary["exchangeQuantity"] 131 | if "trades" in json_dictionary: 132 | self.trades = [TradeForOrder(trade) for trade in json_dictionary["trades"]] 133 | 134 | 135 | # GET /orders 136 | class Orders(BaseSerializedResponse): 137 | def __init__(self, json_dictionary: Dict): 138 | super().__init__(json_dictionary) 139 | 140 | if "orders" in json_dictionary: 141 | self.orders = [OrderDetails(order) for order in json_dictionary["orders"]] 142 | 143 | 144 | # GET /trades 145 | class Trade(BaseSerializedResponse): 146 | def __init__(self, json_dictionary: Dict): 147 | super().__init__(json_dictionary) 148 | 149 | if "id" in json_dictionary: 150 | self.id = json_dictionary["id"] 151 | if "symbol" in json_dictionary: 152 | self.symbol = json_dictionary["symbol"] 153 | if "side" in json_dictionary: 154 | self.side = json_dictionary["side"] 155 | if "tradeAmount" in json_dictionary: 156 | self.trade_amount = json_dictionary["tradeAmount"] 157 | if "executedPrice" in json_dictionary: 158 | self.executed_price = json_dictionary["executedPrice"] 159 | if "timestamp" in json_dictionary: 160 | self.timestamp = json_dictionary["timestamp"] 161 | if "orderId" in json_dictionary: 162 | self.order_id = json_dictionary["orderId"] 163 | 164 | 165 | class TradeHistory(BaseSerializedResponse): 166 | def __init__(self, json_dictionary: Dict): 167 | super().__init__(json_dictionary) 168 | 169 | if "trades" in json_dictionary: 170 | self.trades = [Trade(trade) for trade in json_dictionary["trades"]] 171 | 172 | 173 | class Transaction(BaseSerializedResponse): 174 | def __init__(self, json_dictionary: Dict): 175 | super().__init__(json_dictionary) 176 | 177 | if "transactionId" in json_dictionary: 178 | self.transaction_id = json_dictionary["transactionId"] 179 | if "createDate" in json_dictionary: 180 | self.create_date = json_dictionary["createDate"] 181 | if "assetName" in json_dictionary: 182 | self.asset_name = json_dictionary["assetName"] 183 | if "amount" in json_dictionary: 184 | self.amount = json_dictionary["amount"] 185 | if "type" in json_dictionary: 186 | self.type = json_dictionary["type"] 187 | if "status" in json_dictionary: 188 | self.status = json_dictionary["status"] 189 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.3 2 | requests==2.28.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | import codecs 4 | import os 5 | import re 6 | 7 | with open("requirements.txt") as f: 8 | required = f.read().splitlines() 9 | with codecs.open( 10 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "nexo", "__init__.py"), 11 | "r", 12 | "latin1", 13 | ) as fp: 14 | try: 15 | version = re.findall(r"^__version__ = '([^']+)'\r?$", fp.read(), re.M)[0] 16 | except IndexError: 17 | raise RuntimeError("Unable to determine version.") 18 | 19 | with open("README.md", "r") as fh: 20 | long_description = fh.read() 21 | 22 | setup( 23 | name="python-nexo", 24 | version=version, 25 | packages=["nexo"], 26 | description="Nexo Pro REST API python implementation", 27 | long_description=long_description, 28 | long_description_content_type="text/markdown", 29 | url="https://github.com/guilyx/python-nexo", 30 | author="Erwin Lejeune", 31 | license="MIT", 32 | author_email="erwin.lejeune15@gmail.com", 33 | install_requires=required, 34 | keywords="nexo crypto exchange rest api bitcoin ethereum btc eth neo", 35 | classifiers=[ 36 | "Intended Audience :: Developers", 37 | "License :: OSI Approved :: MIT License", 38 | "Operating System :: OS Independent", 39 | "Programming Language :: Python :: 3", 40 | "Programming Language :: Python :: 3.5", 41 | "Programming Language :: Python :: 3.6", 42 | "Programming Language :: Python :: 3.7", 43 | "Programming Language :: Python", 44 | "Topic :: Software Development :: Libraries :: Python Modules", 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_api_request.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | import pytest 7 | import nexo 8 | import time 9 | 10 | client = nexo.Client("key", "secret") 11 | 12 | 13 | def test_client(): 14 | assert client.API_KEY == "key" 15 | assert client.API_SECRET == "secret" 16 | 17 | 18 | def test_forbidden_call(): 19 | with pytest.raises(nexo.NexoAPIException) as e: 20 | client.get_account_balances() 21 | 22 | time.sleep(1.1) 23 | assert e.value.code == 100 24 | assert ( 25 | str(e.value) 26 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 27 | ) 28 | 29 | 30 | def test_get_trade_history_validity(): 31 | pair = "IDONTEXIST" 32 | with pytest.raises(nexo.NexoRequestException) as e: 33 | client.get_trade_history([pair], None, None, None, None) 34 | 35 | time.sleep(1.1) 36 | assert ( 37 | e.value.message 38 | == f"Bad Request: Tried to get trade history with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 39 | ) 40 | 41 | pair = "ETH/USDT" 42 | with pytest.raises(nexo.NexoAPIException) as e: 43 | client.get_trade_history([pair], None, None, None, None) 44 | 45 | time.sleep(1.1) 46 | assert e.value.code == 100 47 | assert ( 48 | str(e.value) 49 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 50 | ) 51 | 52 | 53 | def test_place_order_wrong_pair(): 54 | pair = "ETHUSDT" 55 | with pytest.raises(nexo.NexoRequestException) as e: 56 | client.place_order(pair, "buy", "market", "10.0") 57 | 58 | assert ( 59 | e.value.message 60 | == f"Bad Request: Tried to place an order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 61 | ) 62 | 63 | 64 | def test_place_order_wrong_type(): 65 | type = "int" 66 | with pytest.raises(nexo.NexoRequestException) as e: 67 | client.place_order("ETH/USDT", "buy", type, "10.0") 68 | 69 | time.sleep(1.1) 70 | assert ( 71 | e.value.message 72 | == f"Bad Request: Tried to place an order with type = {type}, side must be 'market' or 'limit'" 73 | ) 74 | 75 | 76 | def test_place_order_wrong_side(): 77 | side = "tails" 78 | with pytest.raises(nexo.NexoRequestException) as e: 79 | client.place_order("ETH/USDT", side, "market", "10.0") 80 | 81 | time.sleep(1.1) 82 | assert ( 83 | e.value.message 84 | == f"Bad Request: Tried to place an order with side = {side}, side must be 'buy' or 'sell'" 85 | ) 86 | 87 | 88 | def test_place_order_healthy(): 89 | pair = "ETH/USDT" 90 | with pytest.raises(nexo.NexoAPIException) as e: 91 | client.place_order(pair, "buy", "market", "10.0") 92 | 93 | time.sleep(1.1) 94 | assert e.value.code == 100 95 | assert ( 96 | str(e.value) 97 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 98 | ) 99 | 100 | pair = "ETH/USDT" 101 | with pytest.raises(nexo.NexoAPIException) as e: 102 | client.place_order(pair, "sell", "market", "10.0") 103 | 104 | time.sleep(1.1) 105 | assert e.value.code == 100 106 | assert ( 107 | str(e.value) 108 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 109 | ) 110 | 111 | pair = "ETH/USDT" 112 | with pytest.raises(nexo.NexoAPIException) as e: 113 | client.place_order(pair, "buy", "limit", "10.0") 114 | 115 | time.sleep(1.1) 116 | assert e.value.code == 100 117 | assert ( 118 | str(e.value) 119 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 120 | ) 121 | 122 | pair = "ETH/USDT" 123 | with pytest.raises(nexo.NexoAPIException) as e: 124 | client.place_order(pair, "sell", "limit", "10.0") 125 | 126 | time.sleep(1.1) 127 | assert e.value.code == 100 128 | assert ( 129 | str(e.value) 130 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 131 | ) 132 | 133 | 134 | def test_place_trigger_order_wrong_pair(): 135 | pair = "ETHUSDT" 136 | with pytest.raises(nexo.NexoRequestException) as e: 137 | client.place_trigger_order(pair, "buy", "stopLoss", "10.0", "100.0") 138 | 139 | time.sleep(1.1) 140 | assert ( 141 | e.value.message 142 | == f"Bad Request: Tried to place a trigger order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 143 | ) 144 | 145 | 146 | def test_place_trigger_order_wrong_trigger_type(): 147 | trigger_type = "stop_loss_wrong" 148 | with pytest.raises(nexo.NexoRequestException) as e: 149 | client.place_trigger_order("ETH/USDT", "buy", trigger_type, "10.0", "100.0") 150 | 151 | time.sleep(1.1) 152 | assert ( 153 | e.value.message 154 | == f"Bad Request: Tried to place a trigger order with trigger type = {trigger_type}, trigger type must be 'stopLoss' or 'takeProfit' or 'trailing'" 155 | ) 156 | 157 | 158 | def test_place_trigger_order_wrong_side(): 159 | side = "tails" 160 | with pytest.raises(nexo.NexoRequestException) as e: 161 | client.place_trigger_order("ETH/USDT", side, "stopLoss", "10.0", "100.0") 162 | 163 | time.sleep(1.1) 164 | assert ( 165 | e.value.message 166 | == f"Bad Request: Tried to place a trigger order with side = {side}, side must be 'buy' or 'sell'" 167 | ) 168 | 169 | 170 | def test_place_trigger_order_healthy(): 171 | pair = "ETH/USDT" 172 | with pytest.raises(nexo.NexoAPIException) as e: 173 | client.place_trigger_order(pair, "buy", "stopLoss", "10.0", "100.0") 174 | 175 | time.sleep(1.1) 176 | assert e.value.code == 100 177 | assert ( 178 | str(e.value) 179 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 180 | ) 181 | 182 | with pytest.raises(nexo.NexoAPIException) as e: 183 | client.place_trigger_order(pair, "sell", "stopLoss", "10.0", "100.0") 184 | 185 | time.sleep(1.1) 186 | assert e.value.code == 100 187 | assert ( 188 | str(e.value) 189 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 190 | ) 191 | 192 | with pytest.raises(nexo.NexoAPIException) as e: 193 | client.place_trigger_order(pair, "buy", "takeProfit", "10.0", "1000.0") 194 | 195 | time.sleep(1.1) 196 | assert e.value.code == 100 197 | assert ( 198 | str(e.value) 199 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 200 | ) 201 | 202 | with pytest.raises(nexo.NexoAPIException) as e: 203 | client.place_trigger_order(pair, "sell", "trailing", "10.0", "100.0") 204 | 205 | time.sleep(1.1) 206 | assert e.value.code == 100 207 | assert ( 208 | str(e.value) 209 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 210 | ) 211 | 212 | 213 | def test_place_advanced_order_wrong_pair(): 214 | pair = "ETHUSDT" 215 | with pytest.raises(nexo.NexoRequestException) as e: 216 | client.place_advanced_order(pair, "buy", "1.0", "10.0", "100.0") 217 | 218 | time.sleep(1.1) 219 | assert ( 220 | e.value.message 221 | == f"Bad Request: Tried to place an advanced order with pair = {pair}, must be of format [A-Z]{{2,6}}/[A-Z]{{2, 6}}" 222 | ) 223 | 224 | 225 | def test_place_advanced_order_wrong_side(): 226 | side = "tails" 227 | with pytest.raises(nexo.NexoRequestException) as e: 228 | client.place_advanced_order("ETH/USDT", side, "1.0", "10.0", "100.0") 229 | 230 | time.sleep(1.1) 231 | assert ( 232 | e.value.message 233 | == f"Bad Request: Tried to place an advanced order with side = {side}, side must be 'buy' or 'sell'" 234 | ) 235 | 236 | 237 | def test_place_advanced_order_healthy(): 238 | pair = "ETH/USDT" 239 | with pytest.raises(nexo.NexoAPIException) as e: 240 | client.place_advanced_order(pair, "buy", "1.0", "10.0", "100.0") 241 | 242 | time.sleep(1.1) 243 | assert e.value.code == 100 244 | assert ( 245 | str(e.value) 246 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 247 | ) 248 | 249 | with pytest.raises(nexo.NexoAPIException) as e: 250 | client.place_advanced_order(pair, "sell", "1.0", "10.0", "100.0") 251 | 252 | time.sleep(1.1) 253 | assert e.value.code == 100 254 | assert ( 255 | str(e.value) 256 | == "APIError(code=100): API Key doesn't exist, API-Key is malformed or invalid." 257 | ) 258 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | from nexo.helpers import check_pair_validity, compact_json_dict 7 | 8 | 9 | def test_pair_validity_no_slash(): 10 | pair = "BTCUSD" 11 | assert check_pair_validity(pair) == False 12 | 13 | 14 | def test_pair_validity_valid(): 15 | pair = "BTC/USD" 16 | assert check_pair_validity(pair) == True 17 | 18 | 19 | def test_pair_validity_too_short(): 20 | pair = "NORMAL/O" 21 | assert check_pair_validity(pair) == False 22 | 23 | 24 | def test_pair_validity_too_short_but_different(): 25 | pair = "O/NORMAL" 26 | assert check_pair_validity(pair) == False 27 | 28 | 29 | def test_pair_validity_too_long(): 30 | pair = "MORETHANSIXCHARS/NORMAL" 31 | assert check_pair_validity(pair) == False 32 | 33 | 34 | def test_pair_validity_too_long_but_different(): 35 | pair = "NORMAL/MORETHANSIXCHARS" 36 | assert check_pair_validity(pair) == False 37 | 38 | 39 | def test_pair_validity_lower_case(): 40 | pair = "usdc/btc" 41 | assert check_pair_validity(pair) == False 42 | 43 | 44 | def test_pair_validity_lower_upper_case_mix(): 45 | pair = "usdc/BAHIO" 46 | assert check_pair_validity(pair) == False 47 | 48 | 49 | def test_dict_jsonifier_simple(): 50 | dict = {"foo": "bar", "bar": "foo"} 51 | json = compact_json_dict(dict) 52 | expected_json = '{"foo":"bar","bar":"foo"}' 53 | assert json == expected_json 54 | 55 | 56 | def test_dict_jsonifier_double(): 57 | dict = {"foo": 2.0, "bar": "foo"} 58 | json = compact_json_dict(dict) 59 | expected_json = '{"foo":2.0,"bar":"foo"}' 60 | assert json == expected_json 61 | 62 | 63 | def test_dict_jsonifier_empty(): 64 | dict = {} 65 | json = compact_json_dict(dict) 66 | expected_json = "{}" 67 | assert json == expected_json 68 | -------------------------------------------------------------------------------- /tests/test_serialized_responses.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | 6 | from nexo.response_serializers import * 7 | import pytest 8 | 9 | 10 | def test_base_class(): 11 | balances_json = { 12 | "balances": [ 13 | { 14 | "assetName": "BTC", 15 | "totalBalance": "0.00000000", 16 | "availableBalance": "0.00000000", 17 | "lockedBalance": "0.00000000", 18 | "debt": "0.00000000", 19 | "interest": "0.00000000", 20 | } 21 | ] 22 | } 23 | base = BaseSerializedResponse(balances_json) 24 | assert str(base) == str(balances_json) 25 | 26 | 27 | def test_balances(): 28 | balances_json = { 29 | "balances": [ 30 | { 31 | "assetName": "BTC", 32 | "totalBalance": "0.00000000", 33 | "availableBalance": "0.00000000", 34 | "lockedBalance": "0.00000000", 35 | "debt": "0.00000000", 36 | "interest": "0.00000000", 37 | } 38 | ] 39 | } 40 | 41 | balances = Balances(balances_json) 42 | 43 | assert len(balances.balances) == 1 44 | assert balances.balances[0].asset_name == "BTC" 45 | assert balances.balances[0].total_balance == "0.00000000" 46 | assert balances.balances[0].available_balance == "0.00000000" 47 | assert balances.balances[0].locked_balance == "0.00000000" 48 | assert balances.balances[0].interest == "0.00000000" 49 | assert balances.balances[0].debt == "0.00000000" 50 | 51 | balances_json = { 52 | "balances": [ 53 | { 54 | "assetName": "XRP", 55 | "totalBalance": "100.0", 56 | "availableBalance": "2.0", 57 | "lockedBalance": "0.3", 58 | "interest": "0.4", 59 | } 60 | ] 61 | } 62 | 63 | balances = Balances(balances_json) 64 | 65 | assert len(balances.balances) == 1 66 | assert balances.balances[0].asset_name == "XRP" 67 | assert balances.balances[0].total_balance == "100.0" 68 | assert balances.balances[0].available_balance == "2.0" 69 | assert balances.balances[0].locked_balance == "0.3" 70 | assert balances.balances[0].interest == "0.4" 71 | 72 | with pytest.raises(AttributeError): 73 | assert balances.balances[0].debt == "3.0" 74 | 75 | 76 | def test_pairs(): 77 | pairs_json = { 78 | "pairs": ["BNB/USDT", "MKR/BTC"], 79 | "minLimits": {"BNB/USDT": 0.355, "MKR_BTC": 0.002}, 80 | "maxLimits": {"BNB/USDT": 3435.5, "MKR_BTC": 42.4}, 81 | } 82 | 83 | pairs = Pairs(pairs_json) 84 | assert len(pairs.pairs) == 2 85 | assert pairs.pairs[0] == "BNB/USDT" 86 | assert pairs.pairs[1] == "MKR/BTC" 87 | assert pairs.min_limits["BNB/USDT"] == 0.355 88 | 89 | pairs_json = { 90 | "pairs": ["BNB/USDT", "MKR/BTC"], 91 | "min_limits": {"BNB/USDT": 0.355, "MKR_BTC": 0.002}, 92 | "max_limits": {"BNB/USDT": 3435.5, "MKR_BTC": 42.4}, 93 | } 94 | 95 | pairs = Pairs(pairs_json) 96 | 97 | with pytest.raises(AttributeError): 98 | assert pairs.min_limits == {"BNB/USDT": 0.355, "MKR_BTC": 0.002} 99 | 100 | with pytest.raises(AttributeError): 101 | assert pairs.max_limits == {"BNB/USDT": 3435.5, "MKR_BTC": 42.4} 102 | 103 | 104 | def test_quote(): 105 | quote_json = { 106 | "pair": "BNB/USDT", 107 | "amount": "1000.0", 108 | "price": "10.0", 109 | "timestamp": "123424243", 110 | "random": "34343", 111 | } 112 | 113 | quote = Quote(quote_json) 114 | 115 | assert quote.pair == "BNB/USDT" 116 | assert quote.amount == "1000.0" 117 | assert quote.price == "10.0" 118 | assert quote.timestamp == "123424243" 119 | 120 | with pytest.raises(AttributeError): 121 | assert quote.random == "34343" 122 | 123 | 124 | def test_trade_for_order(): 125 | trade_json = { 126 | "id": "234", 127 | "symbol": "NEXO/USDT", 128 | "type": "market", 129 | "orderAmount": "100", 130 | "amountFilled": "100", 131 | "executedPrice": "1.324", 132 | "timestamp": 1314242424, 133 | "status": "completed", 134 | "random": "random", 135 | } 136 | 137 | trade = TradeForOrder(trade_json) 138 | 139 | assert trade.id == "234" 140 | assert trade.symbol == "NEXO/USDT" 141 | assert trade.type == "market" 142 | assert trade.order_amount == "100" 143 | assert trade.amount_filled == "100" 144 | assert trade.executed_price == "1.324" 145 | assert trade.timestamp == 1314242424 146 | assert trade.status == "completed" 147 | 148 | with pytest.raises(AttributeError): 149 | assert trade.random == "random" 150 | 151 | 152 | def test_order_details(): 153 | order_det_json = { 154 | "id": "234", 155 | "pair": "NEXO/USDT", 156 | "side": "buy", 157 | "quantity": "100", 158 | "exchangeRate": "100", 159 | "exchangeQuantity": "1.324", 160 | "timestamp": 1314242424, 161 | "status": "completed", 162 | "random": "random", 163 | "trades": [ 164 | { 165 | "id": "234", 166 | "symbol": "NEXO/USDT", 167 | "type": "market", 168 | "orderAmount": "100", 169 | "amountFilled": "100", 170 | "executedPrice": "1.324", 171 | "timestamp": 1314242424, 172 | "status": "completed", 173 | "random": "random", 174 | }, 175 | { 176 | "id": "237", 177 | "symbol": "NEXO/USDT", 178 | "type": "market", 179 | "orderAmount": "100", 180 | "amountFilled": "100", 181 | "executedPrice": "1.324", 182 | "timestamp": 1314242424, 183 | "status": "completed", 184 | "random": "random", 185 | }, 186 | ], 187 | } 188 | 189 | order_details = OrderDetails(order_det_json) 190 | 191 | assert len(order_details.trades) == 2 192 | assert order_details.trades[0].id == "234" 193 | assert order_details.trades[0].symbol == "NEXO/USDT" 194 | assert order_details.trades[0].type == "market" 195 | assert order_details.trades[0].order_amount == "100" 196 | assert order_details.trades[0].amount_filled == "100" 197 | assert order_details.trades[0].executed_price == "1.324" 198 | assert order_details.trades[0].timestamp == 1314242424 199 | assert order_details.trades[0].status == "completed" 200 | 201 | assert order_details.trades[1].id == "237" 202 | assert order_details.trades[1].symbol == "NEXO/USDT" 203 | assert order_details.trades[1].type == "market" 204 | assert order_details.trades[1].order_amount == "100" 205 | assert order_details.trades[1].amount_filled == "100" 206 | assert order_details.trades[1].executed_price == "1.324" 207 | assert order_details.trades[1].timestamp == 1314242424 208 | assert order_details.trades[1].status == "completed" 209 | 210 | with pytest.raises(AttributeError): 211 | assert order_details.random == "random" 212 | 213 | assert order_details.id == "234" 214 | assert order_details.side == "buy" 215 | assert order_details.exchange_rate == "100" 216 | assert order_details.exchange_quantity == "1.324" 217 | assert order_details.timestamp == 1314242424 218 | 219 | with pytest.raises(AttributeError): 220 | assert order_details.random == "random" 221 | 222 | with pytest.raises(AttributeError): 223 | assert order_details.status == "completed" 224 | --------------------------------------------------------------------------------