├── .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 | 
12 | [](https://github.com/guilyx/python-nexo/actions/workflows/tests.yml)
13 | [](https://codecov.io/gh/guilyx/python-nexo)
14 | [](https://www.codefactor.io/repository/github/guilyx/python-nexo)
15 | [](http://isitmaintained.com/project/guilyx/python-nexo "Percentage of issues still open")
16 | 
17 | [](https://pypi.python.org/pypi/python-nexo/)
18 | [](https://github.com/guilyx/python-nexo/blob/master/LICENSE)
19 | [](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 |
--------------------------------------------------------------------------------