Merge pull request #223 from Davi0kProgramsThings/master

Merge branch `Davi0kProgramsThings:master` into branch `bitfinexcom:master`.
This commit is contained in:
Vigan Abdurrahmani
2023-06-23 09:42:47 +02:00
committed by GitHub
143 changed files with 5456 additions and 8062 deletions

View File

@@ -1,13 +0,0 @@
#### Issue type
- [ ] bug
- [ ] missing functionality
- [ ] performance
- [ ] feature request
#### Brief description
#### Steps to reproduce
-
##### Additional Notes:
-

35
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,35 @@
## I'm submitting a...
- [ ] bug report;
- [ ] feature request;
- [ ] documentation change;
## What is the expected behaviour?
<!--- Describe the expected behaviour in details -->
## What is the current behaviour?
<!--- Describe the current behaviour in details -->
## Possible solution (optional)
<!-- If you have a solution proposal, please explain it here -->
<!-- If your solution includes implementation, you should also open a PR with this as related issue -->
<!-- You can delete this section if you don't want to suggest a possible solution -->
A possible solution could be...
## Steps to reproduce (for bugs)
<!-- You can delete this section if you are not submitting a bug report -->
1. &nbsp;
2. &nbsp;
3. &nbsp;
### Python version
<!-- Indicate your python version here -->
<!-- You can print it using `python3 --version`-->
Python 3.10.6 x64
### Mypy version
<!-- Indicate your mypy version here -->
<!-- You can print it using `python3 -m mypy --version`-->
mypy 0.991 (compiled: yes)

View File

@@ -1,15 +0,0 @@
### Description:
...
### Breaking changes:
- [ ]
### New features:
- [ ]
### Fixes:
- [ ]
### PR status:
- [ ] Version bumped
- [ ] Change-log updated

32
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,32 @@
# Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
## Related Issue
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
PR fixes the following issue:
## Type of change
<!-- Select the most suitable choice and remove the others from the checklist -->
- [ ] Bug fix (non-breaking change which fixes an issue);
- [ ] New feature (non-breaking change which adds functionality);
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected);
- [ ] This change requires a documentation update;
# Checklist:
- [ ] My code follows the style guidelines of this project;
- [ ] I have performed a self-review of my code;
- [ ] I have commented my code, particularly in hard-to-understand areas;
- [ ] I have made corresponding changes to the documentation;
- [ ] My changes generate no new warnings;
- [ ] I have added tests that prove my fix is effective or that my feature works;
- [ ] New and existing unit tests pass locally with my changes;
- [ ] Mypy returns no errors or warnings when run on the root package;
- [ ] Pylint returns a score of 10.00/10.00 when run on the root package;
- [ ] I have updated the library version and updated the CHANGELOG;

View File

@@ -0,0 +1,31 @@
name: bitfinex-api-py-ci
on:
push:
branches:
- master
pull_request:
branches:
- master
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install bitfinex-api-py's dependencies
run: python -m pip install -r dev-requirements.txt
- name: Lint the project with pylint (and fail if score is lower than 10.00/10.00)
run: python -m pylint bfxapi
- name: Run mypy to check the correctness of type hinting (and fail if any error or warning is found)
run: python -m mypy bfxapi
- name: Execute project's unit tests (unittest)
run: python -m unittest bfxapi.tests

View File

@@ -1,36 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python application
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest

12
.gitignore vendored
View File

@@ -1,6 +1,12 @@
__pycache__
*.pyc
.vscode
*.pyc
*.log
dist
bitfinex_api_py.egg-info
__pycache__
dist
venv
!.gitkeep
MANIFEST

33
.pylintrc Normal file
View File

@@ -0,0 +1,33 @@
[MAIN]
py-version=3.8.0
[MESSAGES CONTROL]
disable=
multiple-imports,
missing-docstring,
logging-not-lazy,
logging-fstring-interpolation,
too-few-public-methods,
too-many-public-methods,
too-many-instance-attributes,
dangerous-default-value,
inconsistent-return-statements,
[SIMILARITIES]
min-similarity-lines=6
[VARIABLES]
allowed-redefined-builtins=type,dir,id,all,format,len
[FORMAT]
max-line-length=120
expected-line-ending-format=LF
[BASIC]
good-names=id,on,pl,t,ip,tf,A,B,C,D,E,F
[TYPECHECK]
generated-members=websockets
[STRING]
check-quote-consistency=yes

View File

@@ -1,8 +1,11 @@
language: python
python:
- "3.8.2"
- "3.8.0"
before_install:
- python -m pip install --upgrade pip
install:
- python3.8 -m pip install -r requirements.txt
- pip install -r dev-requirements.txt
script:
- pylint --rcfile=pylint.rc bfxapi
- pytest
- python -m pylint bfxapi
- python -m mypy bfxapi
- python -m unittest bfxapi.tests

172
CHANGELOG
View File

@@ -1,172 +0,0 @@
2.0.6
-) Added id field to _parse_trade
2.0.5
-) Show correct fees for withdraws
2.0.4
-) Added missing ID to ledgers.py
2.0.3
-) Implemented Liquidations endpoint (REST)
2.0.2
-) Use private host for auth-based requests
2.0.1
-) Added User Settings Write/Read/Delete endpoints (REST)
-) Added Balance Available for Orders/Offers endpoint (REST)
-) Added Alerts endpoints (REST)
-) Fixed trades handling error
2.0.0
-) Implemented Movement endpoints (REST)
-) Fixed unawaited stop
-) Changed account's trade execution (te) and trade update (tu) handling
1.3.4
-) Fixed undefined p_sub issue in subscription_manager.py
-) Added submit cancel all funding orders endpoint (REST)
-) Added get all exchange pairs endpoint (REST)
1.3.3
-) Fixed socket.send() issue (IndexError: deque index out of range)
1.3.2
-) Implemented Merchants endpoints (REST)
1.3.1
-) Handle exception of asyncio.get_event_loop() | Related to v1.2.8
1.3.0
-) Adjusted get_trades() to allow symbol to be None and get trades for all symbols
1.2.8
-) Bugfix - It is possible to call bfx.ws.run() from an already running event loop
1.2.7
-) Added ws support for Python 3.9 and 3.10
1.2.6
-) Updated websockets to 9.1
1.2.5
-) Adjusted get_order_history() rest endpoint
1.2.4
-) Added example of MARKET order with price=None
1.2.3
-) Tests adjusted
1.2.2
-) WS bugfix (exception InvalidStatusCode not handled)
1.2.1
-) Added orderbook implementation example (ws)
1.2.0
-) Implemented Margin Info (rest)
-) Implemented claim position (rest)
-) When max_retries == 0 continue forever to retry (websocket)
1.1.15
-) Added 'ids' parameter to get_order_history()
-) Added an example to show how it is possible to spawn multiple bfx ws instances to comply with the open subscriptions number constraint (max. 25)
-) Implemented Funding Trades (rest)
1.1.14
-) bfx_websockets.py ERRORS dictionary now contains a message for error number 10305
1.1.13
-) Adding balance_available to the Wallet.
1.1.12
-) Applied clientside fix to get_public_trades() (in case of multiple trades at the same timestamp they should be ordered by id)
-) Invalid orders are now removed from pending_orders
-) FOK orders cancelled are now removed from pending_orders
1.1.11
-) Removed pendingOrders from BfxWebsocket() (it was not used anywhere)
-) Fixed issue in confirm_order_new() (the keys of the dict pending_orders are the cids of the orders, and not the ids)
1.1.10
- Fixed get_seed_candles() [backwards compatible]
1.1.9
- Implemented PULSE endpoints (rest)
- Updated pyee and changed deprecated class EventEmitter() -> AsyncIOEventEmitter() to make it work with all Python 3.X versions
- Implemented Foreign exchange rate endpoint (rest)
- Implemented Market average price endpoint (rest)
- Implemented Generate invoice endpoint (rest)
- Implemented Keep funding endpoint (rest)
- Implemented Cancel order multi endpoint (rest)
- Implemented Public Stats endpoint (rest)
- Implemented Order Multi OP endpoint (rest)
- Implemented Public Tickers History (rest)
- Implemented Public Funding Stats (rest)
- Updated dependencies in setup.py
1.1.8
- Adds support for websocket events pu, pn and pu
1.1.7
- Adds rest.get_ledgers
1.1.6
- Adds 'new_ticker' websocket event stream
- Adds 'ws.stop' function to kill all websocket connections
- Fixes Position modal loading from raw array
1.1.5
- Fixes 100% CPU utilization bug with the generic_websocket event emitter thread
1.1.4
- Locks mutex when sending websocket messages
- Fix py3.8 stricter linting errors
1.1.3
- Adds ability to specify channel_filter in client
1.1.2
- Adds aff_code to WS and Rest submit order functions
1.1.1
- Rework README with new bfx templates
- Generate /docs/rest_v2.md
- Generate /docs/ws_v2.md
- Update comments for doc generation
- Allow only python3 installs in setup.py
1.1.0
- Adds rest.submit_funding_offer
- Adds rest.submit_cancel_funding_offer
- Adds rest.submit_wallet_transfer
- Adds rest.get_wallet_deposit_address
- Adds rest.create_wallet_deposit_address
- Adds rest.submit_wallet_withdraw
- Adds rest.submit_order
- Adds rest.submit_cancel_order
- Adds rest.submit_update_order
- Updates websocket notification event to use Notfication model object
1.0.1
- Added ws event `status_update`
- Added rest function `get_derivative_status`
- Added rest function `get_derivative_statuses`
- Added rest function `set_derivative_collateral`
- Added channel support `status`
- Added create_event_emitter as override in generic_websocket
for custom event emitters
1.0.0
- Removal of camel-casing file naming and git duplicates
i.e bfx.client instead of bfx.Client
0.0.1
- Added change-log and PR/Issue templates

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@bitfinex.com (Bitfinex).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

26
LICENSE
View File

@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -174,28 +175,3 @@
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

508
README.md
View File

@@ -1,175 +1,413 @@
# Bitfinex Trading Library for Python - Bitcoin, Ethereum, Ripple and more
# bitfinex-api-py (v3-beta)
![https://api.travis-ci.org/bitfinexcom/bitfinex-api-py.svg?branch=master](https://api.travis-ci.org/bitfinexcom/bitfinex-api-py.svg?branch=master)
Official implementation of the [Bitfinex APIs (V2)](https://docs.bitfinex.com/docs) for `Python 3.8+`.
A Python reference implementation of the Bitfinex API for both REST and websocket interaction.
> **DISCLAIMER:** \
Production use of v3.0.0b1 (and all future beta versions) is HIGHLY discouraged. \
Beta versions should not be used in applications which require user authentication. \
Provide your API-KEY/API-SECRET, and manage your account and funds at your own risk.
# Features
- Official implementation
- Websocket V2 and Rest V2
- Connection multiplexing
- Order and wallet management
- All market data feeds
### Features
* Support for 75+ REST endpoints (a list of available endpoints can be found [here](https://docs.bitfinex.com/reference))
* New WebSocket client to ensure fast, secure and persistent connections
* Full support for Bitfinex notifications (including custom notifications)
* Native support for type hinting and type checking with [`mypy`](https://github.com/python/mypy)
## Installation
Clone package into PYTHONPATH:
```sh
git clone https://github.com/bitfinexcom/bitfinex-api-py.git
cd bitfinex-api-py
```console
python3 -m pip install --pre bitfinex-api-py
```
Or via pip:
```sh
python3 -m pip install bitfinex-api-py
### Selecting and installing a specific beta version
It's also possible to select and install a specific beta version:
```console
python3 -m pip install bitfinex-api-py==3.0.0b1
```
Run the trades/candles example:
```sh
cd bfxapi/examples/ws
python3 subscribe_trades_candles.py
---
# Quickstart
```python
from bfxapi import Client, REST_HOST
from bfxapi.types import Notification, Order
bfx = Client(
rest_host=REST_HOST,
api_key="<YOUR BFX API-KEY>",
api_secret="<YOUR BFX API-SECRET>"
)
notification: Notification[Order] = bfx.rest.auth.submit_order(
type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0)
order: Order = notification.data
if notification.status == "SUCCESS":
print(f"Successful new order for {order.symbol} at {order.price}$.")
if notification.status == "ERROR":
raise Exception(f"Something went wrong: {notification.text}")
```
## Quickstart
## Authenticating in your account
To authenticate in your account, you must provide a valid API-KEY and API-SECRET:
```python
bfx = Client(
[...],
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
```
### Warning
Remember to not share your API-KEYs and API-SECRETs with anyone. \
Everyone who owns one of your API-KEYs and API-SECRETs will have full access to your account. \
We suggest saving your credentials in a local `.env` file and accessing them as environment variables.
_Revoke your API-KEYs and API-SECRETs immediately if you think they might have been stolen._
> **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key).
## Next
* [WebSocket client documentation](#websocket-client-documentation)
- [Advanced features](#advanced-features)
- [Examples](#examples)
* [How to contribute](#how-to-contribute)
---
# WebSocket client documentation
1. [Instantiating the client](#instantiating-the-client)
* [Authentication](#authentication)
2. [Running the client](#running-the-client)
* [Closing the connection](#closing-the-connection)
3. [Subscribing to public channels](#subscribing-to-public-channels)
* [Unsubscribing from a public channel](#unsubscribing-from-a-public-channel)
* [Setting a custom `sub_id`](#setting-a-custom-sub_id)
4. [Listening to events](#listening-to-events)
### Advanced features
* [Using custom notifications](#using-custom-notifications)
* [Setting up connection multiplexing](#setting-up-connection-multiplexing)
### Examples
* [Creating a new order](#creating-a-new-order)
## Instantiating the client
```python
bfx = Client(wss_host=PUB_WSS_HOST)
```
`Client::wss` contains an instance of `BfxWebSocketClient` (core implementation of the WebSocket client). \
The `wss_host` argument is used to indicate the URL to which the WebSocket client should connect. \
The `bfxapi` package exports 2 constants to quickly set this URL:
Constant | URL | When to use
:--- | :--- | :---
WSS_HOST | wss://api.bitfinex.com/ws/2 | Suitable for all situations, supports authentication.
PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | For public uses only, doesn't support authentication.
PUB_WSS_HOST is recommended over WSS_HOST for applications that don't require authentication.
> **NOTE:** The `wss_host` parameter is optional, and the default value is WSS_HOST.
### Authentication
To learn how to authenticate in your account, have a look at [Authenticating in your account](#authenticating-in-your-account).
If authentication is successful, the client will emit the `authenticated` event. \
All operations that require authentication will fail if run before the emission of this event. \
The `data` argument contains information about the authentication, such as the `userId`, the `auth_id`, etc...
```python
@bfx.wss.on("authenticated")
def on_authenticated(data: Dict[str, Any]):
print(f"Successful login for user <{data['userId']}>.")
```
`data` can also be useful for checking if an API-KEY has certain permissions:
```python
@bfx.wss.on("authenticated")
def on_authenticated(data: Dict[str, Any]):
if not data["caps"]["orders"]["read"]:
raise Exception("This application requires read permissions on orders.")
if not data["caps"]["positions"]["write"]:
raise Exception("This application requires write permissions on positions.")
```
## Running the client
The client can be run using `BfxWebSocketClient::run`:
```python
bfx.wss.run()
```
If an event loop is already running, users can start the client with `BfxWebSocketClient::start`:
```python
await bfx.wss.start()
```
If the client succeeds in connecting to the server, it will emit the `open` event. \
This is the right place for all bootstrap activities, such as subscribing to public channels. \
To learn more about events and public channels, see [Listening to events](#listening-to-events) and [Subscribing to public channels](#subscribing-to-public-channels).
```python
@bfx.wss.on("open")
async def on_open():
await bfx.wss.subscribe("ticker", symbol="tBTCUSD")
```
### Closing the connection
Users can close the connection with the WebSocket server using `BfxWebSocketClient::close`:
```python
await bfx.wss.close()
```
A custom [close code number](https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number), along with a verbose reason, can be given as parameters:
```python
await bfx.wss.close(code=1001, reason="Going Away")
```
After closing the connection, the client will emit the `disconnection` event:
```python
@bfx.wss.on("disconnection")
def on_disconnection(code: int, reason: str):
if code == 1000 or code == 1001:
print("Closing the connection without errors!")
```
## Subscribing to public channels
Users can subscribe to public channels using `BfxWebSocketClient::subscribe`:
```python
await bfx.wss.subscribe("ticker", symbol="tBTCUSD")
```
On each successful subscription, the client will emit the `subscribed` event:
```python
@bfx.wss.on("subscribed")
def on_subscribed(subscription: subscriptions.Subscription):
if subscription["channel"] == "ticker":
print(f"{subscription['symbol']}: {subscription['subId']}") # tBTCUSD: f2757df2-7e11-4244-9bb7-a53b7343bef8
```
### Unsubscribing from a public channel
It is possible to unsubscribe from a public channel at any time. \
Unsubscribing from a public channel prevents the client from receiving any more data from it. \
This can be done using `BfxWebSocketClient::unsubscribe`, and passing the `sub_id` of the public channel you want to unsubscribe from:
```python
await bfx.wss.unsubscribe(sub_id="f2757df2-7e11-4244-9bb7-a53b7343bef8")
```
### Setting a custom `sub_id`
The client generates a random `sub_id` for each subscription. \
These values must be unique, as the client uses them to identify subscriptions. \
However, it is possible to force this value by passing a custom `sub_id` to `BfxWebSocketClient::subscribe`:
```python
await bfx.wss.subscribe("candles", key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011")
```
## Listening to events
Whenever the WebSocket client receives data, it will emit a specific event. \
Users can either ignore those events or listen for them by registering callback functions. \
These callback functions can also be asynchronous; in fact the client fully supports coroutines ([`asyncio`](https://docs.python.org/3/library/asyncio.html)).
To add a listener for a specific event, users can use the decorator `BfxWebSocketClient::on`:
```python
@bfx.wss.on("candles_update")
def on_candles_update(sub: subscriptions.Candles, candle: Candle):
print(f"Candle update for key <{sub['key']}>: {candle}")
```
The same can be done without using decorators:
```python
bfx.wss.on("candles_update", callback=on_candles_update)
```
You can pass any number of events to register for the same callback function:
```python
bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update)
```
# Advanced features
## Using custom notifications
**Using custom notifications requires user authentication.**
Users can send custom notifications using `BfxWebSocketClient::notify`:
```python
await bfx.wss.notify({ "foo": 1 })
```
Any data can be sent along with a custom notification.
Custom notifications are broadcast by the server on all user's open connections. \
So, each custom notification will be sent to every online client of the current user. \
Whenever a client receives a custom notification, it will emit the `notification` event:
```python
@bfx.wss.on("notification")
def on_notification(notification: Notification[Any]):
print(notification.data) # { "foo": 1 }
```
## Setting up connection multiplexing
`BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument:
```python
bfx.wss.run(connections=3)
```
`connections` indicates the number of connections to run concurrently (through connection multiplexing).
Each of these connections can handle up to 25 subscriptions to public channels. \
So, using `N` connections will allow the client to handle at most `N * 25` subscriptions. \
You should always use the minimum number of connections necessary to handle all the subscriptions that will be made.
For example, if you know that your application will subscribe to 75 public channels, 75 / 25 = 3 connections will be enough to handle all the subscriptions.
The default number of connections is 5; therefore, if the `connections` argument is not given, the client will be able to handle a maximum of 25 * 5 = 125 subscriptions.
Keep in mind that using a large number of connections could slow down the client performance.
The use of more than 20 connections is not recommended.
# Examples
## Creating a new order
```python
import os
import sys
from bfxapi import Client, Order
from bfxapi import Client, WSS_HOST
from bfxapi.types import Notification, Order
bfx = Client(
API_KEY='<YOUR_API_KEY>',
API_SECRET='<YOUR_API_SECRET>'
wss_host=WSS_HOST,
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
@bfx.ws.on('authenticated')
async def submit_order(auth_message):
await bfx.ws.submit_order('tBTCUSD', 19000, 0.01, Order.Type.EXCHANGE_MARKET)
@bfx.wss.on("authenticated")
async def on_authenticated(_):
await bfx.wss.inputs.submit_order(
type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0)
bfx.ws.run()
@bfx.wss.on("order_new")
def on_order_new(order: Order):
print(f"Successful new order for {order.symbol} at {order.price}$.")
@bfx.wss.on("on-req-notification")
def on_notification(notification: Notification[Order]):
if notification.status == "ERROR":
raise Exception(f"Something went wrong: {notification.text}")
bfx.wss.run()
```
## Docs
---
* <b>[V2 Rest](docs/rest_v2.md)</b> - Documentation
* <b>[V2 Websocket](docs/ws_v2.md)</b> - Documentation
# How to contribute
## Examples
All contributions are welcome! :D
#### Authenticate
A guide on how to install and set up `bitfinex-api-py`'s source code can be found [here](#installation-and-setup). \
Before opening any pull requests, please have a look at [Before Opening a PR](#before-opening-a-pr). \
Contributors must uphold the [Contributor Covenant code of conduct](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/CODE_OF_CONDUCT.md).
```python
bfx = Client(
API_KEY='<YOUR_API_KEY>',
API_SECRET='<YOUR_API_SECRET>'
)
### Index
@bfx.ws.on('authenticated')
async def do_something():
print ("Success!")
1. [Installation and setup](#installation-and-setup)
* [Cloning the repository](#cloning-the-repository)
* [Installing the dependencies](#installing-the-dependencies)
2. [Before opening a PR](#before-opening-a-pr)
* [Running the unit tests](#running-the-unit-tests)
3. [License](#license)
bfx.ws.run()
## Installation and setup
A brief guide on how to install and set up the project in your Python 3.8+ environment.
### Cloning the repository
The following command will only clone the `v3-beta` branch (excluding all others):
```console
git clone --branch v3-beta --single-branch https://github.com/bitfinexcom/bitfinex-api-py.git
```
#### Subscribe to trades
### Installing the dependencies
```python
from bfxapi import Client
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET
)
@bfx.ws.on('new_trade')
def log_trade(trade):
print ("New trade: {}".format(trade))
@bfx.ws.on('connected')
def start():
bfx.ws.subscribe('trades', 'tBTCUSD')
bfx.ws.run()
```console
python3 -m pip install -r dev-requirements.txt
```
#### Withdraw from wallet via REST
Make sure to install `dev-requirements.txt` instead of `requirements.txt`. \
`dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependencies. \
This will also install the versions in use of [`pylint`](https://github.com/pylint-dev/pylint) and [`mypy`](https://github.com/python/mypy), which you should both use before opening your PRs.
```python
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG'
)
response = await bfx.rest.submit_wallet_withdraw("exchange", "tetheruse", 5, "0xc5bbb852f82c24327693937d4012f496cff7eddf")
print ("Address: ", response.notify_info)
All done, your Python 3.8+ environment should now be able to run `bitfinex-api-py`'s source code.
## Before opening a PR
**We won't accept your PR or we will request changes if the following requirements aren't met.**
Wheter you're submitting a bug fix, a new feature or a documentation change, you should first discuss it in an issue.
All PRs must follow this [PULL_REQUEST_TEMPLATE](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/.github/PULL_REQUEST_TEMPLATE.md) and include an exhaustive description.
Before opening a pull request, you should also make sure that:
- [ ] all unit tests pass (see [Running the unit tests](#running-the-unit-tests)).
- [ ] [`pylint`](https://github.com/pylint-dev/pylint) returns a score of 10.00/10.00 when run against your code.
- [ ] [`mypy`](https://github.com/python/mypy) doesn't throw any error code when run on the project (excluding notes).
### Running the unit tests
`bitfinex-api-py` comes with a set of unit tests (written using the [`unittest`](https://docs.python.org/3.8/library/unittest.html) unit testing framework). \
Contributors must ensure that each unit test passes before opening a pull request. \
You can run all project's unit tests by calling `unittest` on `bfxapi.tests`:
```console
python3 -m unittest -v bfxapi.tests
```
See the <b>[examples](https://github.com/bitfinexcom/bitfinex-api-py/tree/master/examples)</b> directory for more, like:
- [Creating/updating an order](https://github.com/bitfinexcom/bitfinex-api-py/blob/master/bfxapi/examples/ws/send_order.py)
- [Subscribing to orderbook updates](https://github.com/bitfinexcom/bitfinex-api-py/blob/master/bfxapi/examples/ws/resubscribe_orderbook.py)
- [Withdrawing crypto](https://github.com/bitfinexcom/bitfinex-api-py/blob/master/bfxapi/examples/rest/transfer_wallet.py)
- [Submitting a funding offer](https://github.com/bitfinexcom/bitfinex-api-py/blob/master/bfxapi/examples/rest/create_funding.py)
A single unit test can be run as follows:
```console
python3 -m unittest -v bfxapi.tests.test_notification
```
For more info on how to use this library please see the example scripts in the `bfxapi/examples` directory. Here you will find usage of all interface exposed functions for both the rest and websocket.
Also please see [this medium article](https://medium.com/@Bitfinex/15f201ad20d4) for a tutorial.
## FAQ
### Is there any rate limiting?
For a Websocket connection there is no limit to the number of requests sent down the connection (unlimited order operations) however an account can only create 15 new connections every 5 mins and each connection is only able to subscribe to 30 inbound data channels. Fortunately this library handles all of the load balancing/multiplexing for channels and will automatically create/destroy new connections when needed, however the user may still encounter the max connections rate limiting error.
For rest the base limit per-user is 1,000 orders per 5 minute interval, and is shared between all account API connections. It increases proportionally to your trade volume based on the following formula:
1000 + (TOTAL_PAIRS_PLATFORM * 60 * 5) / (250000000 / USER_VOL_LAST_30d)
Where TOTAL_PAIRS_PLATFORM is the number of pairs on the Bitfinex platform (currently ~101) and USER_VOL_LAST_30d is in USD.
### Will I always receive an `on` packet?
No; if your order fills immediately, the first packet referencing the order will be an `oc` signaling the order has closed. If the order fills partially immediately after creation, an `on` packet will arrive with a status of `PARTIALLY FILLED...`
For example, if you submit a `LIMIT` buy for 0.2 BTC and it is added to the order book, an `on` packet will arrive via ws2. After a partial fill of 0.1 BTC, an `ou` packet will arrive, followed by a final `oc` after the remaining 0.1 BTC fills.
On the other hand, if the order fills immediately for 0.2 BTC, you will only receive an `oc` packet.
### My websocket won't connect!
Did you call `client.Connect()`? :)
### nonce too small
I make multiple parallel request and I receive an error that the nonce is too small. What does it mean?
Nonces are used to guard against replay attacks. When multiple HTTP requests arrive at the API with the wrong nonce, e.g. because of an async timing issue, the API will reject the request.
If you need to go parallel, you have to use multiple API keys right now.
### How do `te` and `tu` messages differ?
A `te` packet is sent first to the client immediately after a trade has been matched & executed, followed by a `tu` message once it has completed processing. During times of high load, the `tu` message may be noticably delayed, and as such only the `te` message should be used for a realtime feed.
### What are the sequence numbers for?
If you enable sequencing on v2 of the WS API, each incoming packet will have a public sequence number at the end, along with an auth sequence number in the case of channel `0` packets. The public seq numbers increment on each packet, and the auth seq numbers increment on each authenticated action (new orders, etc). These values allow you to verify that no packets have been missed/dropped, since they always increase monotonically.
### What is the difference between R* and P* order books?
Order books with precision `R0` are considered 'raw' and contain entries for each order submitted to the book, whereas `P*` books contain entries for each price level (which aggregate orders).
## Contributing
1. Fork it ( https://github.com/[my-github-username]/bitfinex/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
### Publish to Pypi
## License
```
python setup.py sdist
twine upload dist/*
Copyright 2023 Bitfinex
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```

View File

@@ -1,14 +1,6 @@
"""
This module is used to interact with the bitfinex api
"""
from .client import Client
from .urls import REST_HOST, PUB_REST_HOST, \
WSS_HOST, PUB_WSS_HOST
from .version import __version__
from .client import Client, PUB_REST_HOST, PUB_WS_HOST, REST_HOST, WS_HOST
from .models import (Order, Trade, OrderBook, Subscription, Wallet,
Position, FundingLoan, FundingOffer, FundingCredit,
Movement)
from .websockets.generic_websocket import GenericWebsocket, Socket
from .websockets.bfx_websocket import BfxWebsocket
from .utils.decimal import Decimal
NAME = 'bfxapi'

View File

@@ -1,24 +1,37 @@
"""
This module exposes the core bitfinex clients which includes both
a websocket client and a rest interface client
"""
from typing import List, Literal, Optional
# pylint: disable-all
from .websockets.bfx_websocket import BfxWebsocket
from .rest.bfx_rest import BfxRest
from .constants import *
from .rest import BfxRestInterface
from .websocket import BfxWebSocketClient
from .urls import REST_HOST, WSS_HOST
class Client:
"""
The bfx client exposes rest and websocket objects
"""
def __init__(
self,
api_key: Optional[str] = None,
api_secret: Optional[str] = None,
filters: Optional[List[str]] = None,
*,
rest_host: str = REST_HOST,
wss_host: str = WSS_HOST,
wss_timeout: Optional[float] = 60 * 15,
log_filename: Optional[str] = None,
log_level: Literal["ERROR", "WARNING", "INFO", "DEBUG"] = "INFO"
):
credentials = None
if api_key and api_secret:
credentials = { "api_key": api_key, "api_secret": api_secret, "filters": filters }
self.rest = BfxRestInterface(
host=rest_host,
credentials=credentials
)
self.wss = BfxWebSocketClient(
host=wss_host,
credentials=credentials,
wss_timeout=wss_timeout,
log_filename=log_filename,
log_level=log_level
)
def __init__(self, API_KEY=None, API_SECRET=None, rest_host=REST_HOST,
ws_host=WS_HOST, create_event_emitter=None, logLevel='INFO', dead_man_switch=False,
ws_capacity=25, channel_filter=[], *args, **kwargs):
self.ws = BfxWebsocket(API_KEY=API_KEY, API_SECRET=API_SECRET, host=ws_host,
logLevel=logLevel, dead_man_switch=dead_man_switch, channel_filter=channel_filter,
ws_capacity=ws_capacity, create_event_emitter=create_event_emitter, *args, **kwargs)
self.rest = BfxRest(API_KEY=API_KEY, API_SECRET=API_SECRET, host=rest_host,
logLevel=logLevel, *args, **kwargs)

View File

@@ -1,4 +0,0 @@
REST_HOST = 'https://api.bitfinex.com/v2'
WS_HOST = 'wss://api.bitfinex.com/ws/2'
PUB_REST_HOST = 'https://api-pub.bitfinex.com/v2'
PUB_WS_HOST = 'wss://api-pub.bitfinex.com/ws/2'

50
bfxapi/enums.py Normal file
View File

@@ -0,0 +1,50 @@
from enum import Enum
class OrderType(str, Enum):
LIMIT = "LIMIT"
EXCHANGE_LIMIT = "EXCHANGE LIMIT"
MARKET = "MARKET"
EXCHANGE_MARKET = "EXCHANGE MARKET"
STOP = "STOP"
EXCHANGE_STOP = "EXCHANGE STOP"
STOP_LIMIT = "STOP LIMIT"
EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT"
TRAILING_STOP = "TRAILING STOP"
EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP"
FOK = "FOK"
EXCHANGE_FOK = "EXCHANGE FOK"
IOC = "IOC"
EXCHANGE_IOC = "EXCHANGE IOC"
class FundingOfferType(str, Enum):
LIMIT = "LIMIT"
FRR_DELTA_FIX = "FRRDELTAFIX"
FRR_DELTA_VAR = "FRRDELTAVAR"
class Flag(int, Enum):
HIDDEN = 64
CLOSE = 512
REDUCE_ONLY = 1024
POST_ONLY = 4096
OCO = 16384
NO_VAR_RATES = 524288
class Error(int, Enum):
ERR_UNK = 10000
ERR_GENERIC = 10001
ERR_CONCURRENCY = 10008
ERR_PARAMS = 10020
ERR_CONF_FAIL = 10050
ERR_AUTH_FAIL = 10100
ERR_AUTH_PAYLOAD = 10111
ERR_AUTH_SIG = 10112
ERR_AUTH_HMAC = 10113
ERR_AUTH_NONCE = 10114
ERR_UNAUTH_FAIL = 10200
ERR_SUB_FAIL = 10300
ERR_SUB_MULTI = 10301
ERR_SUB_UNK = 10302
ERR_SUB_LIMIT = 10305
ERR_UNSUB_FAIL = 10400
ERR_UNSUB_NOT = 10401
ERR_READY = 11000

View File

@@ -1,38 +0,0 @@
import os
import sys
import asyncio
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Create funding requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
async def create_funding():
response = await bfx.rest.submit_funding_offer("fUSD", 1000, 0.012, 7)
# response is in the form of a Notification object
# notify_info is in the form of a FundingOffer
print ("Offer: ", response.notify_info)
async def cancel_funding():
response = await bfx.rest.submit_cancel_funding_offer(41235958)
# response is in the form of a Notification object
# notify_info is in the form of a FundingOffer
print ("Offer: ", response.notify_info)
async def run():
await create_funding()
await cancel_funding()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,47 +0,0 @@
import os
import sys
import asyncio
import time
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
from bfxapi.models import OrderType
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Create order requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
async def create_order():
response = await bfx.rest.submit_order(symbol="tBTCUSD", amount=10, price=None, market_type=OrderType.MARKET)
# response is in the form of a Notification object
for o in response.notify_info:
# each item is in the form of an Order object
print ("Order: ", o)
async def cancel_order():
response = await bfx.rest.submit_cancel_order(1185510865)
# response is in the form of a Notification object
# notify_info is in the form of an order object
print ("Order: ", response.notify_info)
async def update_order():
response = await bfx.rest.submit_update_order(1185510771, price=15, amount=0.055)
# response is in the form of a Notification object
# notify_info is in the form of an order object
print ("Order: ", response.notify_info)
async def run():
await create_order()
await cancel_order()
await update_order()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,110 +0,0 @@
import os
import sys
import asyncio
import time
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Retrieving authenticated data requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
now = int(round(time.time() * 1000))
then = now - (1000 * 60 * 60 * 24 * 10) # 10 days ago
async def log_wallets():
wallets = await bfx.rest.get_wallets()
print ("Wallets:")
[ print (w) for w in wallets ]
async def log_active_orders():
orders = await bfx.rest.get_active_orders('tBTCUSD')
print ("Orders:")
[ print (o) for o in orders ]
async def log_orders_history():
orders = await bfx.rest.get_order_history('tBTCUSD', 0, then)
print ("Orders:")
[ print (o) for o in orders ]
async def log_active_positions():
positions = await bfx.rest.get_active_position()
print ("Positions:")
[ print (p) for p in positions ]
async def log_trades():
trades = await bfx.rest.get_trades(symbol='tBTCUSD', start=0, end=then)
print ("Trades:")
[ print (t) for t in trades]
async def log_order_trades():
order_id = 1151353463
trades = await bfx.rest.get_order_trades('tBTCUSD', order_id)
print ("Trade orders:")
[ print (t) for t in trades]
async def log_funding_offers():
offers = await bfx.rest.get_funding_offers('fBTC')
print ("Offers:")
[ print (o) for o in offers]
async def log_funding_offer_history():
offers = await bfx.rest.get_funding_offer_history('fBTC', 0, then)
print ("Offers history:")
[ print (o) for o in offers]
async def log_funding_loans():
loans = await bfx.rest.get_funding_loans('fBTC')
print ("Funding loans:")
[ print (l) for l in loans ]
async def log_funding_loans_history():
loans = await bfx.rest.get_funding_loan_history('fBTC', 0, then)
print ("Funding loan history:")
[ print (l) for l in loans ]
async def log_funding_credits():
credits = await bfx.rest.get_funding_credits('fBTC')
print ("Funding credits:")
[ print (c) for c in credits ]
async def log_funding_credits_history():
credit = await bfx.rest.get_funding_credit_history('fBTC', 0, then)
print ("Funding credit history:")
[ print (c) for c in credit ]
async def log_margin_info():
margin_info = await bfx.rest.get_margin_info('tBTCUSD')
print(margin_info)
sym_all = await bfx.rest.get_margin_info('sym_all') # list of Margin Info
for margin_info in sym_all:
print(margin_info)
base = await bfx.rest.get_margin_info('base')
print(base)
async def run():
await log_wallets()
await log_active_orders()
await log_orders_history()
await log_active_positions()
await log_trades()
await log_order_trades()
await log_funding_offers()
await log_funding_offer_history()
await log_funding_credits()
await log_funding_credits_history()
await log_margin_info()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,21 +0,0 @@
import os
import sys
import asyncio
import time
sys.path.append('../../../')
from bfxapi import Client, PUB_REST_HOST
bfx = Client(
logLevel='INFO',
rest_host=PUB_REST_HOST
)
now = int(round(time.time() * 1000))
then = now - (1000 * 60 * 60 * 24 * 10) # 10 days ago
async def get_liquidations():
liquidations = await bfx.rest.get_liquidations(start=then, end=now)
print(liquidations)
asyncio.get_event_loop().run_until_complete(get_liquidations())

View File

@@ -1,59 +0,0 @@
import os
import sys
import asyncio
import time
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving public data requires public hosts
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
now = int(round(time.time() * 1000))
then = now - (1000 * 60 * 60 * 24 * 10) # 10 days ago
async def log_historical_candles():
candles = await bfx.rest.get_public_candles('tBTCUSD', 0, then)
print ("Candles:")
[ print (c) for c in candles ]
async def log_historical_trades():
trades = await bfx.rest.get_public_trades('tBTCUSD', 0, then)
print ("Trades:")
[ print (t) for t in trades ]
async def log_books():
orders = await bfx.rest.get_public_books('tBTCUSD')
print ("Order book:")
[ print (o) for o in orders ]
async def log_ticker():
ticker = await bfx.rest.get_public_ticker('tBTCUSD')
print ("Ticker:")
print (ticker)
async def log_mul_tickers():
tickers = await bfx.rest.get_public_tickers(['tBTCUSD', 'tETHBTC'])
print ("Tickers:")
print (tickers)
async def log_derivative_status():
status = await bfx.rest.get_derivative_status('tBTCF0:USTF0')
print ("Deriv status:")
print (status)
async def run():
await log_historical_candles()
await log_historical_trades()
await log_books()
await log_ticker()
await log_mul_tickers()
await log_derivative_status()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,20 +0,0 @@
import os
import sys
import asyncio
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving seed trades requires public hosts
bfx = Client(
logLevel='INFO',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
async def get_seeds():
candles = await bfx.rest.get_seed_candles('tBTCUSD')
print (candles)
asyncio.get_event_loop().run_until_complete(get_seeds())

View File

@@ -1,37 +0,0 @@
import os
import sys
import asyncio
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Submitting invoices requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
async def run():
await bfx.rest.submit_invoice(amount='2.0', currency='USD', pay_currencies=['BTC', 'ETH'], order_id='order123', webhook='https://example.com/api/v3/order/order123',
redirect_url='https://example.com/api/v3/order/order123', customer_info_nationality='DE',
customer_info_resid_country='GB', customer_info_resid_city='London', customer_info_resid_zip_code='WC2H 7NA',
customer_info_resid_street='5-6 Leicester Square', customer_info_resid_building_no='23 A',
customer_info_full_name='John Doe', customer_info_email='john@example.com', duration=86339)
invoices = await bfx.rest.get_invoices()
print(invoices)
# await bfx.rest.complete_invoice(id=invoices[0]['id'], pay_ccy='BTC', deposit_id=1357996)
unlinked_deposits = await bfx.rest.get_unlinked_deposits(ccy='BTC')
print(unlinked_deposits)
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,52 +0,0 @@
import os
import sys
import asyncio
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Transfer wallet requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
async def transfer_wallet():
response = await bfx.rest.submit_wallet_transfer("exchange", "margin", "BTC", "BTC", 0.1)
# response is in the form of a Notification object
# notify_info is in the form of a Transfer object
print ("Transfer: ", response.notify_info)
async def deposit_address():
response = await bfx.rest.get_wallet_deposit_address("exchange", "bitcoin")
# response is in the form of a Notification object
# notify_info is in the form of a DepositAddress object
print ("Address: ", response.notify_info)
async def create_new_address():
response = await bfx.rest.create_wallet_deposit_address("exchange", "bitcoin")
# response is in the form of a Notification object
# notify_info is in the form of a DepositAddress object
print ("Address: ", response.notify_info)
async def withdraw():
# tetheruse = Tether (ERC20)
response = await bfx.rest.submit_wallet_withdraw("exchange", "tetheruse", 5, "0xc5bbb852f82c24327693937d4012f496cff7eddf")
# response is in the form of a Notification object
# notify_info is in the form of a DepositAddress object
print ("Address: ", response.notify_info)
async def run():
await transfer_wallet()
await deposit_address()
await withdraw()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,40 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client, Order
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Canceling orders requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
@bfx.ws.on('order_closed')
def order_cancelled(order):
print ("Order cancelled.")
print (order)
@bfx.ws.on('order_confirmed')
async def trade_completed(order):
print ("Order confirmed.")
print (order)
await bfx.ws.cancel_order(order.id)
@bfx.ws.on('error')
def log_error(msg):
print ("Error: {}".format(msg))
@bfx.ws.once('authenticated')
async def submit_order(auth_message):
# create an initial order at a really low price so it stays open
await bfx.ws.submit_order('tBTCUSD', 10, 1, Order.Type.EXCHANGE_LIMIT)
bfx.ws.run()

View File

@@ -1,22 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
@bfx.ws.on('error')
def log_error(msg):
print ("Error: {}".format(msg))
@bfx.ws.on('all')
async def log_output(output):
print ("WS: {}".format(output))
bfx.ws.run()

View File

@@ -1,27 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST,
dead_man_switch=True, # <-- kill all orders if this connection drops
channel_filter=['wallet'] # <-- only receive wallet updates
)
@bfx.ws.on('error')
def log_error(msg):
print ("Error: {}".format(msg))
@bfx.ws.on('authenticated')
async def submit_order(auth_message):
print ("Authenticated!!")

View File

@@ -1,83 +0,0 @@
import sys
import time
from collections import OrderedDict
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving orderbook requires public hosts
bfx = Client(
manageOrderBooks=True,
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
class OrderBook:
def __init__(self, snapshot):
self.bids = OrderedDict()
self.asks = OrderedDict()
self.load(snapshot)
def load(self, snapshot):
for record in snapshot:
if record[2] >= 0:
self.bids[record[0]] = {
'count': record[1],
'amount': record[2]
}
else:
self.asks[record[0]] = {
'count': record[1],
'amount': record[2]
}
def update(self, record):
# count is 0
if record[1] == 0:
if record[2] == 1:
# remove from bids
del self.bids[record[0]]
elif record[2] == -1:
# remove from asks
del self.asks[record[0]]
elif record[1] > 0:
if record[2] > 0:
# update bids
if record[0] not in self.bids:
self.bids[record[0]] = {}
self.bids[record[0]]['count'] = record[1]
self.bids[record[0]]['amount'] = record[2]
elif record[2] < 0:
# update asks
if record[0] not in self.asks:
self.asks[record[0]] = {}
self.asks[record[0]]['count'] = record[1]
self.asks[record[0]]['amount'] = record[2]
obs = {}
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('order_book_update')
def log_update(data):
obs[data['symbol']].update(data['data'])
@bfx.ws.on('order_book_snapshot')
def log_snapshot(data):
obs[data['symbol']] = OrderBook(data['data'])
async def start():
await bfx.ws.subscribe('book', 'tBTCUSD')
bfx.ws.on('connected', start)
bfx.ws.run()
for n in range(0, 10):
time.sleep(2)
for key in obs:
print(f"Printing {key} orderbook...")
print(f"{obs[key].bids}\n")
print(f"{obs[key].asks}\n")

View File

@@ -1,180 +0,0 @@
"""
This is an example of how it is possible to spawn multiple
bfx ws instances to comply with the open subscriptions number constraint (max. 25)
(https://docs.bitfinex.com/docs/requirements-and-limitations)
"""
import sys
sys.path.append('../../../')
import asyncio
from functools import partial
import websockets as ws
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
import math
import random
MAX_CHANNELS = 25
def get_random_list_of_tickers():
tickers = ["FILUST", "FTTUSD", "FTTUST", "FUNUSD", "GNOUSD", "GNTUSD", "GOTEUR", "GOTUSD", "GTXUSD", "ZRXUSD"]
return random.sample(tickers, 1)
class Instance:
def __init__(self, _id):
self.id = _id
self.bfx = Client(logLevel='INFO', ws_host=PUB_WS_HOST, rest_host=PUB_REST_HOST)
self.subscriptions = {'trades': {}, 'ticker': {}}
self.is_ready = False
def run(self):
self.bfx.ws.run()
self.bfx.ws.on('error', log_error)
self.bfx.ws.on('new_trade', log_trade)
self.bfx.ws.on('new_ticker', log_ticker)
self.bfx.ws.on('subscribed', partial(on_subscribe, self))
self.bfx.ws.on('unsubscribed', partial(on_unsubscribed, self))
self.bfx.ws.on('connected', partial(on_connected, self))
self.bfx.ws.on('stopped', partial(on_stopped, self))
async def subscribe(self, symbols):
for symbol in symbols:
print(f'Subscribing to {symbol} channel')
await self.bfx.ws.subscribe_ticker(symbol)
await self.bfx.ws.subscribe_trades(symbol)
self.subscriptions['trades'][symbol] = None
self.subscriptions['ticker'][symbol] = None
async def unsubscribe(self, symbols):
for symbol in symbols:
if symbol in self.subscriptions['trades']:
print(f'Unsubscribing to {symbol} channel')
trades_ch_id = self.subscriptions['trades'][symbol]
ticker_ch_id = self.subscriptions['ticker'][symbol]
if trades_ch_id:
await self.bfx.ws.unsubscribe(trades_ch_id)
else:
del self.subscriptions['trades'][symbol]
if ticker_ch_id:
await self.bfx.ws.unsubscribe(ticker_ch_id)
else:
del self.subscriptions['ticker'][symbol]
class Routine:
is_stopped = False
def __new__(cls, _loop, _ws, interval=1, start_delay=10):
instance = super().__new__(cls)
instance.interval = interval
instance.start_delay = start_delay
instance.ws = _ws
instance.task = _loop.create_task(instance.run())
return instance.task
async def run(self):
await asyncio.sleep(self.start_delay)
await self.do()
while True:
await asyncio.sleep(self.interval)
await self.do()
async def do(self):
subbed_tickers = get_all_subscriptions_tickers()
print(f'Subscribed tickers: {subbed_tickers}')
# if ticker is not in subbed tickers, then we subscribe to the channel
to_sub = [f"t{ticker}" for ticker in get_random_list_of_tickers() if f"t{ticker}" not in subbed_tickers]
for ticker in to_sub:
print(f'To subscribe: {ticker}')
instance = get_available_instance()
if instance and instance.is_ready:
print(f'Subscribing on instance {instance.id}')
await instance.subscribe([ticker])
else:
instances_to_create = math.ceil(len(to_sub) / MAX_CHANNELS)
create_instances(instances_to_create)
break
to_unsub = [f"t{ticker}" for ticker in subbed_tickers if f"t{ticker}" in get_random_list_of_tickers()]
if len(to_unsub) > 0:
print(f'To unsubscribe: {to_unsub}')
for instance in instances:
await instance.unsubscribe(to_unsub)
def stop(self):
self.task.cancel()
self.is_stopped = True
instances = []
def get_all_subscriptions_tickers():
tickers = []
for instance in instances:
for ticker in instance.subscriptions['trades']:
tickers.append(ticker)
return tickers
def count_open_channels(instance):
return len(instance.subscriptions['trades']) + len(instance.subscriptions['ticker'])
def create_instances(instances_to_create):
for _ in range(0, instances_to_create):
instance = Instance(len(instances))
instance.run()
instances.append(instance)
def get_available_instance():
for instance in instances:
if count_open_channels(instance) + 1 <= MAX_CHANNELS:
return instance
return None
def log_error(err):
print("Error: {}".format(err))
def log_trade(trade):
print(trade)
def log_ticker(ticker):
print(ticker)
async def on_subscribe(instance, subscription):
print(f'Subscribed to {subscription.symbol} channel {subscription.channel_name}')
instance.subscriptions[subscription.channel_name][subscription.symbol] = subscription.chan_id
async def on_unsubscribed(instance, subscription):
print(f'Unsubscribed to {subscription.symbol} channel {subscription.channel_name}')
instance.subscriptions[subscription.channel_name][subscription.symbol] = subscription.chan_id
del instance.subscriptions[subscription.channel_name][subscription.symbol]
async def on_connected(instance):
print(f"Instance {instance.id} is connected")
instance.is_ready = True
async def on_stopped(instance):
print(f"Instance {instance.id} is dead, removing it from instances list")
instances.pop(instance.id)
def run():
loop = asyncio.get_event_loop()
task = Routine(loop, ws, interval=5)
loop.run_until_complete(task)
run()

View File

@@ -1,40 +0,0 @@
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving orderbook requires public hosts
bfx = Client(
manageOrderBooks=True,
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('unsubscribed')
async def on_unsubscribe(subscription):
print ("Unsubscribed from {}".format(subscription.symbol))
# await subscription.subscribe()
@bfx.ws.on('subscribed')
async def on_subscribe(subscription):
print ("Subscribed to {}".format(subscription.symbol))
# await subscription.unsubscribe()
# or
# await bfx.ws.unsubscribe(subscription.chanId)
@bfx.ws.once('subscribed')
async def on_once_subscribe(subscription):
print ("Performig resubscribe")
await bfx.ws.resubscribe(subscription.chan_id)
async def start():
await bfx.ws.subscribe('book', 'tBTCUSD')
bfx.ws.on('connected', start)
bfx.ws.run()

View File

@@ -1,51 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client, Order
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Sending order requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
@bfx.ws.on('order_snapshot')
async def cancel_all(data):
await bfx.ws.cancel_all_orders()
@bfx.ws.on('order_confirmed')
async def trade_completed(order):
print ("Order confirmed.")
print (order)
## close the order
# await order.close()
# or
# await bfx.ws.cancel_order(order.id)
# or
# await bfx.ws.cancel_all_orders()
@bfx.ws.on('error')
def log_error(msg):
print ("Error: {}".format(msg))
@bfx.ws.on('authenticated')
async def submit_order(auth_message):
await bfx.ws.submit_order(symbol='tBTCUSD', price=None, amount=0.01, market_type=Order.Type.EXCHANGE_MARKET)
# If you dont want to use a decorator
# ws.on('authenticated', submit_order)
# ws.on('error', log_error)
# You can also provide a callback
# await ws.submit_order('tBTCUSD', 0, 0.01,
# 'EXCHANGE MARKET', onClose=trade_complete)
bfx.ws.run()

View File

@@ -1,23 +0,0 @@
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
@bfx.ws.on('order_book_snapshot')
async def log_snapshot(data):
print ("Snapshot: {}".format(data))
# stop the websocket once a snapshot is received
await bfx.ws.stop()
async def start():
await bfx.ws.subscribe('book', 'tBTCUSD')
bfx.ws.on('connected', start)
bfx.ws.run()

View File

@@ -1,26 +0,0 @@
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving derivative status requires public hosts
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('status_update')
def log_msg(msg):
print (msg)
async def start():
await bfx.ws.subscribe_derivative_status('tBTCF0:USTF0')
bfx.ws.on('connected', start)
bfx.ws.run()

View File

@@ -1,35 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving trades/candles requires public hosts
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST,
# Verifies that the local orderbook is up to date
# with the bitfinex servers
manageOrderBooks=True
)
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('order_book_update')
def log_update(data):
print ("Book update: {}".format(data))
@bfx.ws.on('order_book_snapshot')
def log_snapshot(data):
print ("Initial book: {}".format(data))
async def start():
await bfx.ws.subscribe('book', 'tBTCUSD')
# bfx.ws.subscribe('book', 'tETHUSD')
bfx.ws.on('connected', start)
bfx.ws.run()

View File

@@ -1,26 +0,0 @@
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving tickers requires public hosts
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('new_funding_ticker')
def log_ticker(ticker):
print ("New ticker: {}".format(ticker))
async def start():
await bfx.ws.subscribe('ticker', 'fUSD')
bfx.ws.on('connected', start)
bfx.ws.run()

View File

@@ -1,35 +0,0 @@
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving trades/candles requires public hosts
bfx = Client(
logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('new_candle')
def log_candle(candle):
print ("New candle: {}".format(candle))
@bfx.ws.on('new_trade')
def log_trade(trade):
print ("New trade: {}".format(trade))
@bfx.ws.on('new_user_trade')
def log_user_trade(trade):
print ("New user trade: {}".format(trade))
async def start():
await bfx.ws.subscribe('candles', 'tBTCUSD', timeframe='1m')
await bfx.ws.subscribe('trades', 'tBTCUSD')
bfx.ws.on('connected', start)
bfx.ws.run()

View File

@@ -1,45 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client, Order
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Update order requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='INFO',
ws_host=WS_HOST,
rest_host=REST_HOST
)
@bfx.ws.on('order_update')
def order_updated(order):
print ("Order updated.")
print (order)
@bfx.ws.once('order_update')
async def order_once_updated(order):
# update a second time using the object function
await order.update(price=80, amount=0.02, flags="2nd update")
@bfx.ws.once('order_confirmed')
async def trade_completed(order):
print ("Order confirmed.")
print (order)
await bfx.ws.update_order(order.id, price=100, amount=0.01)
@bfx.ws.on('error')
def log_error(msg):
print ("Error: {}".format(msg))
@bfx.ws.once('authenticated')
async def submit_order(auth_message):
# create an inital order a really low price so it stays open
await bfx.ws.submit_order('tBTCUSD', 10, 1, Order.Type.EXCHANGE_LIMIT)
bfx.ws.run()

View File

@@ -1,34 +0,0 @@
import os
import sys
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Checking wallet balances requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='INFO',
ws_host=WS_HOST,
rest_host=REST_HOST
)
@bfx.ws.on('wallet_snapshot')
def log_snapshot(wallets):
for wallet in wallets:
print (wallet)
# or bfx.ws.wallets.get_wallets()
@bfx.ws.on('wallet_update')
def log_update(wallet):
print ("Balance updates: {}".format(wallet))
@bfx.ws.on('error')
def log_error(msg):
print ("Error: {}".format(msg))
bfx.ws.run()

8
bfxapi/exceptions.py Normal file
View File

@@ -0,0 +1,8 @@
__all__ = [
"BfxBaseException",
]
class BfxBaseException(Exception):
"""
Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py.
"""

View File

@@ -1,27 +0,0 @@
"""
This module contains a group of different models which
are used to define data types
"""
from .order import Order
from .trade import Trade
from .order_book import OrderBook
from .subscription import Subscription
from .wallet import Wallet
from .position import Position
from .funding_loan import FundingLoan
from .funding_offer import FundingOffer
from .funding_credit import FundingCredit
from .notification import Notification
from .transfer import Transfer
from .deposit_address import DepositAddress
from .withdraw import Withdraw
from .ticker import Ticker
from .funding_ticker import FundingTicker
from .ledger import Ledger
from .funding_trade import FundingTrade
from .margin_info import MarginInfo
from .margin_info_base import MarginInfoBase
from .movement import Movement
NAME = "models"

View File

@@ -1,44 +0,0 @@
"""
Module used to describe a DepositAddress object
"""
class DepositModel:
"""
Enum used to index the location of each value in a raw array
"""
METHOD = 1
CURRENCY = 2
ADDRESS = 4
class DepositAddress:
"""
[None, 'BITCOIN', 'BTC', None, '38zsUkv8q2aiXK9qsZVwepXjWeh3jKvvZw']
METHOD string Protocol used for funds transfer
SYMBOL string Currency symbol
ADDRESS string Deposit address for funds transfer
"""
def __init__(self, method, currency, address):
self.method = method
self.currency = currency
self.address = address
@staticmethod
def from_raw_deposit_address(raw_add):
"""
Parse a raw deposit object into a DepositAddress object
@return DepositAddress
"""
method = raw_add[DepositModel.METHOD]
currency = raw_add[DepositModel.CURRENCY]
address = raw_add[DepositModel.ADDRESS]
return DepositAddress(method, currency, address)
def __str__(self):
"""
Allow us to print the Transfer object in a pretty format
"""
text = "DepositAddress <{} method={} currency={}>"
return text.format(self.address, self.method, self.currency)

View File

@@ -1,104 +0,0 @@
"""
Module used to describe all of the different data types
"""
class FundingCreditModel:
"""
Enum used to index the location of each value in a raw array
"""
ID = 0
SYMBOL = 1
SIDE = 2
MTS_CREATE = 3
MTS_UPDATE = 4
AMOUNT = 5
FLAGS = 6
STATUS = 7
RATE = 11
PERIOD = 12
MTS_OPENING = 13
MTS_LAST_PAYOUT = 14
NOTIFY = 15
HIDDEN = 16
RENEW = 18
NO_CLOSE = 20
POSITION_PAIR = 21
class FundingCredit:
"""
ID integer Offer ID
SYMBOL string The currency of the offer (fUSD, etc)
SIDE string "Lend" or "Loan"
MTS_CREATE int Millisecond Time Stamp when the offer was created
MSG_UPDATE int Millisecond Time Stamp when the offer was updated
AMOUNT float Amount the offer is for
FLAGS object future params object (stay tuned)
STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED
RATE float Rate of the offer
PERIOD int Period of the offer
MTS_OPENING int Millisecond Time Stamp when funding opened
MTS_LAST_PAYOUT int Millisecond Time Stamp when last payout received
NOTIFY int 0 if false, 1 if true
HIDDEN int 0 if false, 1 if true
RENEW int 0 if false, 1 if true
NO_CLOSE int 0 if false, 1 if true Whether the funding will be closed when the
position is closed
POSITION_PAIR string Pair of the position that the funding was used for
"""
def __init__(self, fid, symbol, side, mts_create, mts_update, amount, flags, status, rate,
period, mts_opening, mts_last_payout, notify, hidden, renew, no_close,
position_pair):
# pylint: disable=invalid-name
self.id = fid
self.symbol = symbol
self.side = side
self.mts_create = mts_create
self.mts_update = mts_update
self.amount = amount
self.flags = flags
self.status = status
self.rate = rate
self.period = period
self.mts_opening = mts_opening
self.mts_last_payout = mts_last_payout
self.notify = notify
self.hidden = hidden
self.renew = renew
self.no_close = no_close
self.position_pair = position_pair
@staticmethod
def from_raw_credit(raw_credit):
"""
Parse a raw credit object into a FundingCredit object
@return FundingCredit
"""
fid = raw_credit[FundingCreditModel.ID]
symbol = raw_credit[FundingCreditModel.SYMBOL]
side = raw_credit[FundingCreditModel.SIDE]
mts_create = raw_credit[FundingCreditModel.MTS_CREATE]
mts_update = raw_credit[FundingCreditModel.MTS_UPDATE]
amount = raw_credit[FundingCreditModel.AMOUNT]
flags = raw_credit[FundingCreditModel.FLAGS]
status = raw_credit[FundingCreditModel.STATUS]
rate = raw_credit[FundingCreditModel.RATE]
period = raw_credit[FundingCreditModel.PERIOD]
mts_opening = raw_credit[FundingCreditModel.MTS_OPENING]
mts_last_payout = raw_credit[FundingCreditModel.MTS_LAST_PAYOUT]
notify = raw_credit[FundingCreditModel.NOTIFY]
hidden = raw_credit[FundingCreditModel.HIDDEN]
renew = raw_credit[FundingCreditModel.RENEW]
no_close = raw_credit[FundingCreditModel.NO_CLOSE]
position_pair = raw_credit[FundingCreditModel.POSITION_PAIR]
return FundingCredit(fid, symbol, side, mts_create, mts_update, amount,
flags, status, rate, period, mts_opening, mts_last_payout,
notify, hidden, renew, no_close, position_pair)
def __str__(self):
string = "FundingCredit '{}' <id={} rate={} amount={} period={} status='{}'>"
return string.format(self.symbol, self.id, self.rate, self.amount,
self.period, self.status)

View File

@@ -1,96 +0,0 @@
"""
Module used to describe all of the different data types
"""
class FundingLoanModel:
"""
Enum used to index the location of each value in a raw array
"""
ID = 0
SYMBOL = 1
SIDE = 2
MTS_CREATE = 3
MTS_UPDATE = 4
AMOUNT = 5
FLAGS = 6
STATUS = 7
RATE = 11
PERIOD = 12
MTS_OPENING = 13
MTS_LAST_PAYOUT = 14
NOTIFY = 15
HIDDEN = 16
RENEW = 18
NO_CLOSE = 20
class FundingLoan:
"""
ID integer Offer ID
SYMBOL string The currency of the offer (fUSD, etc)
SIDE string "Lend" or "Loan"
MTS_CREATE int Millisecond Time Stamp when the offer was created
MTS_UPDATE int Millisecond Time Stamp when the offer was created
AMOUNT float Amount the offer is for
FLAGS object future params object (stay tuned)
STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED
RATE float Rate of the offer
PERIOD int Period of the offer
MTS_OPENING int Millisecond Time Stamp for when the loan was opened
MTS_LAST_PAYOUT int Millisecond Time Stamp for when the last payout was made
NOTIFY int 0 if false, 1 if true
HIDDEN int 0 if false, 1 if true
RENEW int 0 if false, 1 if true
NO_CLOSE int If funding will be returned when position is closed. 0 if false, 1 if true
"""
def __init__(self, fid, symbol, side, mts_create, mts_update, amount, flags, status, rate,
period, mts_opening, mts_last_payout, notify, hidden, renew, no_close):
# pylint: disable=invalid-name
self.id = fid
self.symbol = symbol
self.side = side
self.mts_create = mts_create
self.mts_update = mts_update
self.amount = amount
self.flags = flags
self.status = status
self.rate = rate
self.period = period
self.mts_opening = mts_opening
self.mts_last_payout = mts_last_payout
self.notify = notify
self.hidden = hidden
self.renew = renew
self.no_close = no_close
@staticmethod
def from_raw_loan(raw_loan):
"""
Parse a raw funding load into a FundingLoan object
@return FundingLoan
"""
fid = raw_loan[FundingLoanModel.ID]
symbol = raw_loan[FundingLoanModel.SYMBOL]
side = raw_loan[FundingLoanModel.SIDE]
mts_create = raw_loan[FundingLoanModel.MTS_CREATE]
mts_update = raw_loan[FundingLoanModel.MTS_UPDATE]
amount = raw_loan[FundingLoanModel.AMOUNT]
flags = raw_loan[FundingLoanModel.FLAGS]
status = raw_loan[FundingLoanModel.STATUS]
rate = raw_loan[FundingLoanModel.RATE]
period = raw_loan[FundingLoanModel.PERIOD]
mts_opening = raw_loan[FundingLoanModel.MTS_OPENING]
mts_last_payout = raw_loan[FundingLoanModel.MTS_LAST_PAYOUT]
notify = raw_loan[FundingLoanModel.NOTIFY]
hidden = raw_loan[FundingLoanModel.HIDDEN]
renew = raw_loan[FundingLoanModel.RENEW]
no_close = raw_loan[FundingLoanModel.NO_CLOSE]
return FundingLoan(fid, symbol, side, mts_create, mts_update, amount, flags, status, rate,
period, mts_opening, mts_last_payout, notify, hidden, renew, no_close)
def __str__(self):
return "FundingLoan '{}' <id={} rate={} amount={} period={} status='{}'>".format(
self.symbol, self.id, self.rate, self.amount, self.period, self.status)

View File

@@ -1,96 +0,0 @@
"""
Module used to describe all of the different data types
"""
class FundingOfferTypes:
"""
Enum used to define the different funding offer types
"""
LIMIT = 'LIMIT'
FRR_DELTA = 'FRRDELTAVAR'
class FundingOfferModel:
"""
Enum used to index the location of each value in a raw array
"""
ID = 0
SYMBOL = 1
MTS_CREATE = 2
MTS_UPDATED = 3
AMOUNT = 4
AMOUNT_ORIG = 5
TYPE = 6
FLAGS = 9
STATUS = 10
RATE = 14
PERIOD = 15
NOTFIY = 16
HIDDEN = 17
RENEW = 19
class FundingOffer:
"""
ID integer Offer ID
SYMBOL string The currency of the offer (fUSD, etc)
MTS_CREATED int Millisecond Time Stamp when the offer was created
MSG_UPDATED int Millisecond Time Stamp when the offer was created
AMOUNT float Amount the offer is for
AMOUNT_ORIG float Amount the offer was entered with originally
TYPE string "lend" or "loan"
FLAGS object future params object (stay tuned)
STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED
RATE float Rate of the offer
PERIOD int Period of the offer
NOTIFY int 0 if false, 1 if true
HIDDEN int 0 if false, 1 if true
RENEW int 0 if false, 1 if true
"""
Type = FundingOfferTypes()
def __init__(self, fid, symbol, mts_create, mts_updated, amount, amount_orig, f_type,
flags, status, rate, period, notify, hidden, renew):
# pylint: disable=invalid-name
self.id = fid
self.symbol = symbol
self.mts_create = mts_create
self.mts_updated = mts_updated
self.amount = amount
self.amount_orig = amount_orig
self.f_type = f_type
self.flags = flags
self.status = status
self.rate = rate
self.period = period
self.notify = notify
self.hidden = hidden
self.renew = renew
@staticmethod
def from_raw_offer(raw_offer):
"""
Parse a raw funding offer into a RawFunding object
@return FundingOffer
"""
oid = raw_offer[FundingOfferModel.ID]
symbol = raw_offer[FundingOfferModel.SYMBOL]
mts_create = raw_offer[FundingOfferModel.MTS_CREATE]
mts_updated = raw_offer[FundingOfferModel.MTS_UPDATED]
amount = raw_offer[FundingOfferModel.AMOUNT]
amount_orig = raw_offer[FundingOfferModel.AMOUNT_ORIG]
f_type = raw_offer[FundingOfferModel.TYPE]
flags = raw_offer[FundingOfferModel.FLAGS]
status = raw_offer[FundingOfferModel.STATUS]
rate = raw_offer[FundingOfferModel.RATE]
period = raw_offer[FundingOfferModel.PERIOD]
notify = raw_offer[FundingOfferModel.NOTFIY]
hidden = raw_offer[FundingOfferModel.HIDDEN]
renew = raw_offer[FundingOfferModel.RENEW]
return FundingOffer(oid, symbol, mts_create, mts_updated, amount,
amount_orig, f_type, flags, status, rate, period, notify, hidden, renew)
def __str__(self):
return "FundingOffer '{}' <id={} rate={} period={} status='{}'>".format(
self.symbol, self.id, self.rate, self.period, self.status)

View File

@@ -1,93 +0,0 @@
"""
Module used to describe all of the different data types
"""
class FundingTickerModel:
"""
Enum used to index the different values in a raw funding ticker array
"""
FRR = 0
BID = 1
BID_PERIOD = 2
BID_SIZE = 3
ASK = 4
ASK_PERIOD = 5
ASK_SIZE = 6
DAILY_CHANGE = 7
DAILY_CHANGE_PERC = 8
LAST_PRICE = 9
VOLUME = 10
HIGH = 11
LOW = 12
# _PLACEHOLDER,
# _PLACEHOLDER,
FRR_AMOUNT_AVAILABLE = 15
class FundingTicker:
"""
FRR float Flash Return Rate - average of all fixed rate funding over the last hour
(funding tickers only)
BID float Price of last highest bid
BID_PERIOD int Bid period covered in days (funding tickers only)
BID_SIZE float Sum of the 25 highest bid sizes
ASK float Price of last lowest ask
ASK_PERIOD int Ask period covered in days (funding tickers only)
ASK_SIZE float Sum of the 25 lowest ask sizes
DAILY_CHANGE float Amount that the last price has changed since yesterday
DAILY_CHANGE_RELATIVE float Relative price change since yesterday
(*100 for percentage change)
LAST_PRICE float Price of the last trade
VOLUME float Daily volume
HIGH float Daily high
LOW float Daily low
FRR_AMOUNT_AVAILABLE float The amount of funding that is available at the
Flash Return Rate (funding tickers only)
"""
def __init__(self, pair, frr, bid, bid_period, bid_size, ask, ask_period, ask_size,
daily_change, daily_change_perc, last_price, volume, high, low, frr_amount_avail):
self.pair = pair
self.frr = frr
self.bid = bid
self.bid_period = bid_period
self.bid_size = bid_size
self.ask = ask
self.ask_period = ask_period
self.ask_size = ask_size
self.daily_change = daily_change
self.daily_change_perc = daily_change_perc
self.last_price = last_price
self.volume = volume
self.high = high
self.low = low
self.frr_amount_available = frr_amount_avail
@staticmethod
def from_raw_ticker(raw_ticker, pair):
"""
Generate a Ticker object from a raw ticker array
"""
# [72128,[6914.5,28.123061460000002,6914.6,22.472037289999996,175.8,0.0261,6915.7,
# 6167.26141685,6964.2,6710.8]]
return FundingTicker(
pair,
raw_ticker[FundingTickerModel.FRR],
raw_ticker[FundingTickerModel.BID],
raw_ticker[FundingTickerModel.BID_PERIOD],
raw_ticker[FundingTickerModel.BID_SIZE],
raw_ticker[FundingTickerModel.ASK],
raw_ticker[FundingTickerModel.ASK_PERIOD],
raw_ticker[FundingTickerModel.ASK_SIZE],
raw_ticker[FundingTickerModel.DAILY_CHANGE],
raw_ticker[FundingTickerModel.DAILY_CHANGE_PERC],
raw_ticker[FundingTickerModel.LAST_PRICE],
raw_ticker[FundingTickerModel.VOLUME],
raw_ticker[FundingTickerModel.HIGH],
raw_ticker[FundingTickerModel.LOW],
raw_ticker[FundingTickerModel.FRR_AMOUNT_AVAILABLE]
)
def __str__(self):
return "FundingTicker '{}' <last='{}' volume={}>".format(
self.pair, self.last_price, self.volume)

View File

@@ -1,55 +0,0 @@
"""
Module used to describe all of the different data types
"""
class FundingTradeModel:
"""
Enum used to index the different values in a raw funding trade array
"""
ID = 0
SYMBOL = 1
MTS_CREATE = 2
OFFER_ID = 3
AMOUNT = 4
RATE = 5
PERIOD = 6
class FundingTrade:
"""
ID integer Offer ID
SYMBOL string The currency of the offer (fUSD, etc)
MTS_CREATE int Millisecond Time Stamp when the offer was created
OFFER_ID int The ID of the offer
AMOUNT float Amount the offer is for
RATE float Rate of the offer
PERIOD int Period of the offer
"""
def __init__(self, tid, symbol, mts_create, offer_id, amount, rate, period):
self.tid = tid
self.symbol = symbol
self.mts_create = mts_create
self.offer_id = offer_id
self.amount = amount
self.rate = rate
self.period = period
@staticmethod
def from_raw_rest_trade(raw_trade):
"""
Generate a Ticker object from a raw ticker array
"""
# [[636040,"fUST",1574077528000,41237922,-100,0.0024,2,null]]
return FundingTrade(
raw_trade[FundingTradeModel.ID],
raw_trade[FundingTradeModel.SYMBOL],
raw_trade[FundingTradeModel.MTS_CREATE],
raw_trade[FundingTradeModel.OFFER_ID],
raw_trade[FundingTradeModel.AMOUNT],
raw_trade[FundingTradeModel.RATE],
raw_trade[FundingTradeModel.PERIOD]
)
def __str__(self):
return "FundingTrade '{}' x {} @ {} for {} days".format(
self.symbol, self.amount, self.rate, self.period)

View File

@@ -1,56 +0,0 @@
"""
Module used to describe a ledger object
"""
class LedgerModel:
"""
Enum used to index the location of each value in a raw array
"""
ID = 0
CURRENCY = 1
MTS = 3
AMOUNT = 5
BALANCE = 6
DESCRIPTION = 8
class Ledger:
"""
ID int
CURRENCY string Currency (BTC, etc)
PLACEHOLDER
MTS int Millisecond Time Stamp of the update
PLACEHOLDER
AMOUNT string Amount of funds to ledger
BALANCE string Amount of funds to ledger
PLACEHOLDER
DESCRIPTION
"""
def __init__(self, lid, currency, mts, amount, balance, description):
self.id = lid
self.currency = currency
self.mts = mts
self.amount = amount
self.balance = balance
self.description = description
@staticmethod
def from_raw_ledger(raw_ledger):
"""
Parse a raw ledger object into a Ledger object
@return Ledger
"""
lid = raw_ledger[LedgerModel.ID]
currency = raw_ledger[LedgerModel.CURRENCY]
mts = raw_ledger[LedgerModel.MTS]
amount = raw_ledger[LedgerModel.AMOUNT]
balance = raw_ledger[LedgerModel.BALANCE]
description = raw_ledger[LedgerModel.DESCRIPTION]
return Ledger(lid, currency, mts, amount, balance, description)
def __str__(self):
''' Allow us to print the Ledger object in a pretty format '''
text = "Ledger <{} {} {} balance:{} '{}' mts={}>"
return text.format(self.id, self.amount, self.currency, self.balance,
self.description, self.mts)

View File

@@ -1,47 +0,0 @@
"""
Module used to describe all of the different data types
"""
import datetime
class MarginInfoModel:
"""
Enum used to index the different values in a raw margin info array
"""
TRADABLE_BALANCE = 0
GROSS_BALANCE = 1
BUY = 2
SELL = 3
class MarginInfo:
"""
SYMBOL string
TRADABLE BALANCE float
GROSS_BALANCE float
BUY
SELL
"""
def __init__(self, symbol, tradable_balance, gross_balance, buy, sell):
# pylint: disable=invalid-name
self.symbol = symbol
self.tradable_balance = tradable_balance
self.gross_balance = gross_balance
self.buy = buy
self.sell = sell
@staticmethod
def from_raw_margin_info(raw_margin_info):
"""
Generate a MarginInfo object from a raw margin info array
"""
symbol = raw_margin_info[1]
tradable_balance = raw_margin_info[2][MarginInfoModel.TRADABLE_BALANCE]
gross_balance = raw_margin_info[2][MarginInfoModel.GROSS_BALANCE]
buy = raw_margin_info[2][MarginInfoModel.BUY]
sell = raw_margin_info[2][MarginInfoModel.SELL]
return MarginInfo(symbol, tradable_balance, gross_balance, buy, sell)
def __str__(self):
return "Margin Info {} buy={} sell={} tradable_balance={} gross_balance={}" \
"".format(self.symbol, self.buy, self.sell, self. tradable_balance, self. gross_balance)

View File

@@ -1,48 +0,0 @@
"""
Module used to describe all of the different data types
"""
import datetime
class MarginInfoBaseModel:
"""
Enum used to index the different values in a raw margin info array
"""
USER_PL = 0
USER_SWAPS = 1
MARGIN_BALANCE = 2
MARGIN_NET = 3
MARGIN_MIN = 4
class MarginInfoBase:
"""
USER_PL float
USER_SWAPS float
MARGIN_BALANCE float
MARGIN_NET float
MARGIN_MIN float
"""
def __init__(self, user_pl, user_swaps, margin_balance, margin_net, margin_min):
# pylint: disable=invalid-name
self.user_pl = user_pl
self.user_swaps = user_swaps
self.margin_balance = margin_balance
self.margin_net = margin_net
self.margin_min = margin_min
@staticmethod
def from_raw_margin_info(raw_margin_info):
"""
Generate a MarginInfoBase object from a raw margin info array
"""
user_pl = raw_margin_info[1][MarginInfoBaseModel.USER_PL]
user_swaps = raw_margin_info[1][MarginInfoBaseModel.USER_SWAPS]
margin_balance = raw_margin_info[1][MarginInfoBaseModel.MARGIN_BALANCE]
margin_net = raw_margin_info[1][MarginInfoBaseModel.MARGIN_NET]
margin_min = raw_margin_info[1][MarginInfoBaseModel.MARGIN_MIN]
return MarginInfoBase(user_pl, user_swaps, margin_balance, margin_net, margin_min)
def __str__(self):
return "Margin Info Base user_pl={} user_swaps={} margin_balance={} margin_net={} margin_min={}" \
"".format(self.user_pl, self.user_swaps, self.margin_balance, self.margin_net, self.margin_min)

View File

@@ -1,76 +0,0 @@
"""
Module used to describe movement data types
"""
import time
import datetime
class MovementModel:
"""
Enum used index the different values in a raw movement array
"""
ID = 0
CURRENCY = 1
CURRENCY_NAME = 2
MTS_STARTED = 5
MTS_UPDATED = 6
STATUS = 9
AMOUNT = 12
FEES = 13
DESTINATION_ADDRESS = 16
TRANSACTION_ID = 20
class Movement:
"""
ID String Movement identifier
CURRENCY String The symbol of the currency (ex. "BTC")
CURRENCY_NAME String The extended name of the currency (ex. "BITCOIN")
MTS_STARTED Date Movement started at
MTS_UPDATED Date Movement last updated at
STATUS String Current status
AMOUNT String Amount of funds moved
FEES String Tx Fees applied
DESTINATION_ADDRESS String Destination address
TRANSACTION_ID String Transaction identifier
"""
def __init__(self, mid, currency, mts_started, mts_updated, status, amount, fees, dst_address, tx_id):
self.id = mid
self.currency = currency
self.mts_started = mts_started
self.mts_updated = mts_updated
self.status = status
self.amount = amount
self.fees = fees
self.dst_address = dst_address
self.tx_id = tx_id
self.date = datetime.datetime.fromtimestamp(mts_started/1000.0)
@staticmethod
def from_raw_movement(raw_movement):
"""
Parse a raw movement object into a Movement object
@return Movement
"""
mid = raw_movement[MovementModel.ID]
currency = raw_movement[MovementModel.CURRENCY]
mts_started = raw_movement[MovementModel.MTS_STARTED]
mts_updated = raw_movement[MovementModel.MTS_UPDATED]
status = raw_movement[MovementModel.STATUS]
amount = raw_movement[MovementModel.AMOUNT]
fees = raw_movement[MovementModel.FEES]
dst_address = raw_movement[MovementModel.DESTINATION_ADDRESS]
tx_id = raw_movement[MovementModel.TRANSACTION_ID]
return Movement(mid, currency, mts_started, mts_updated, status, amount, fees, dst_address, tx_id)
def __str__(self):
''' Allow us to print the Movement object in a pretty format '''
text = "Movement <'{}' amount={} fees={} mts_created={} mts_updated={} status='{}' destination_address={} transaction_id={}>"
return text.format(self.currency, self.amount, self.fees,
self.mts_started, self.mts_updated, self.status, self.dst_address, self.tx_id)

View File

@@ -1,121 +0,0 @@
"""
Module used to describe all of the different notification data types
"""
from .order import Order
from .funding_offer import FundingOffer
from .transfer import Transfer
from .deposit_address import DepositAddress
from .withdraw import Withdraw
class NotificationModal:
"""
Enum used index the different values in a raw order array
"""
MTS = 0
TYPE = 1
MESSAGE_ID = 2
NOTIFY_INFO = 4
CODE = 5
STATUS = 6
TEXT = 7
class NotificationError:
"""
Enum used to hold the error response statuses
"""
SUCCESS = "SUCCESS"
ERROR = "ERROR"
FAILURE = "FAILURE"
class NotificationTypes:
"""
Enum used to hold the different notification types
"""
ORDER_NEW_REQ = "on-req"
ORDER_CANCELED_REQ = "oc-req"
ORDER_UPDATED_REQ = "ou-req"
FUNDING_OFFER_NEW = "fon-req"
FUNDING_OFFER_CANCEL = "foc-req"
ACCOUNT_TRANSFER = "acc_tf"
ACCOUNT_DEPOSIT = "acc_dep"
ACCOUNT_WITHDRAW_REQ = "acc_wd-req"
# uca ?
# pm-req ?
class Notification:
"""
MTS int Millisecond Time Stamp of the update
TYPE string Purpose of notification ('on-req', 'oc-req', 'uca', 'fon-req', 'foc-req')
MESSAGE_ID int unique ID of the message
NOTIFY_INFO array/object A message containing information regarding the notification
CODE null or integer Work in progress
STATUS string Status of the notification; it may vary over time (SUCCESS, ERROR, FAILURE, ...)
TEXT string Text of the notification
"""
def __init__(self, mts, notify_type, message_id, notify_info, code, status, text):
self.mts = mts
self.notify_type = notify_type
self.message_id = message_id
self.notify_info = notify_info
self.code = code
self.status = status
self.text = text
def is_success(self):
"""
Check if the notification status was a success.
@return bool: True if is success else False
"""
if self.status == NotificationError.SUCCESS:
return True
return False
@staticmethod
def from_raw_notification(raw_notification):
"""
Parse a raw notification object into an Order object
@return Notification
"""
mts = raw_notification[NotificationModal.MTS]
notify_type = raw_notification[NotificationModal.TYPE]
message_id = raw_notification[NotificationModal.MESSAGE_ID]
notify_info = raw_notification[NotificationModal.NOTIFY_INFO]
code = raw_notification[NotificationModal.CODE]
status = raw_notification[NotificationModal.STATUS]
text = raw_notification[NotificationModal.TEXT]
basic = Notification(mts, notify_type, message_id, notify_info, code,
status, text)
# if failure notification then just return as is
if not basic.is_success():
return basic
# parse additional notification data
if basic.notify_type == NotificationTypes.ORDER_NEW_REQ:
basic.notify_info = Order.from_raw_order_snapshot(basic.notify_info)
elif basic.notify_type == NotificationTypes.ORDER_CANCELED_REQ:
basic.notify_info = Order.from_raw_order(basic.notify_info)
elif basic.notify_type == NotificationTypes.ORDER_UPDATED_REQ:
basic.notify_info = Order.from_raw_order(basic.notify_info)
elif basic.notify_type == NotificationTypes.FUNDING_OFFER_NEW:
basic.notify_info = FundingOffer.from_raw_offer(basic.notify_info)
elif basic.notify_type == NotificationTypes.FUNDING_OFFER_CANCEL:
basic.notify_info = FundingOffer.from_raw_offer(basic.notify_info)
elif basic.notify_type == NotificationTypes.ACCOUNT_TRANSFER:
basic.notify_info = Transfer.from_raw_transfer(basic.notify_info)
elif basic.notify_type == NotificationTypes.ACCOUNT_DEPOSIT:
basic.notify_info = DepositAddress.from_raw_deposit_address(basic.notify_info)
elif basic.notify_type == NotificationTypes.ACCOUNT_WITHDRAW_REQ:
basic.notify_info = Withdraw.from_raw_withdraw(basic.notify_info)
return basic
def __str__(self):
"""
Allow us to print the Notification object in a pretty format
"""
text = "Notification <'{}' ({}) - {} notify_info={}>"
return text.format(self.notify_type, self.status, self.text, self.notify_info)

View File

@@ -1,238 +0,0 @@
"""
Module used to describe all of the different order data types
"""
import time
import datetime
class OrderType:
"""
Enum used to describe all of the different order types available for use
"""
MARKET = 'MARKET'
LIMIT = 'LIMIT'
STOP = 'STOP'
STOP_LIMIT = 'STOP LIMIT'
TRAILING_STOP = 'TRAILING STOP'
FILL_OR_KILL = 'FOK'
EXCHANGE_MARKET = 'EXCHANGE MARKET'
EXCHANGE_LIMIT = 'EXCHANGE LIMIT'
EXCHANGE_STOP = 'EXCHANGE STOP'
EXCHANGE_STOP_LIMIT = 'EXCHANGE STOP LIMIT'
EXCHANGE_TRAILING_STOP = 'EXCHANGE TRAILING STOP'
EXCHANGE_FILL_OR_KILL = 'EXCHANGE FOK'
LIMIT_ORDERS = [OrderType.LIMIT, OrderType.STOP_LIMIT, OrderType.EXCHANGE_LIMIT,
OrderType.EXCHANGE_STOP_LIMIT, OrderType.FILL_OR_KILL,
OrderType.EXCHANGE_FILL_OR_KILL]
class OrderSide:
"""
Enum used to describe the different directions of an order
"""
BUY = 'buy'
SELL = 'sell'
class OrderClosedModel:
"""
Enum used to index the different values in a raw order array
"""
ID = 0
GID = 1
CID = 2
SYMBOL = 3
MTS_CREATE = 4
MTS_UPDATE = 5
AMOUNT = 6
AMOUNT_ORIG = 7
TYPE = 8
TYPE_PREV = 9
FLAGS = 12
STATUS = 13
PRICE = 16
PRICE_AVG = 17
PRICE_TRAILING = 18
PRICE_AUX_LIMIT = 19
NOTIFY = 23
PLACE_ID = 25
META = 31
class OrderFlags:
"""
Enum used to explain the different values that can be passed in
as flags
"""
HIDDEN = 64
CLOSE = 512
REDUCE_ONLY = 1024
POST_ONLY = 4096
OCO = 16384
def now_in_mills():
"""
Gets the current time in milliseconds
"""
return int(round(time.time() * 1000))
class Order:
"""
ID int64 Order ID
GID int Group ID
CID int Client Order ID
SYMBOL string Pair (tBTCUSD, ...)
MTS_CREATE int Millisecond timestamp of creation
MTS_UPDATE int Millisecond timestamp of update
AMOUNT float Positive means buy, negative means sell.
AMOUNT_ORIG float Original amount
TYPE string The type of the order: LIMIT, MARKET, STOP, TRAILING STOP,
EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP, EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK.
TYPE_PREV string Previous order type
FLAGS int Upcoming Params Object (stay tuned)
ORDER_STATUS string Order Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED
PRICE float Price
PRICE_AVG float Average price
PRICE_TRAILING float The trailing price
PRICE_AUX_LIMIT float Auxiliary Limit price (for STOP LIMIT)
HIDDEN int 1 if Hidden, 0 if not hidden
PLACED_ID int If another order caused this order to be placed (OCO) this will be that other
order's ID
"""
Type = OrderType()
Side = OrderSide()
Flags = OrderFlags()
def __init__(self, oid, gid, cid, symbol, mts_create, mts_update, amount,
amount_orig, o_type, typePrev, flags, status, price, price_avg,
price_trailing, price_aux_limit, notfiy, place_id, meta):
self.id = oid # pylint: disable=invalid-name
self.gid = gid
self.cid = cid
self.symbol = symbol
self.mts_create = mts_create
self.mts_update = mts_update
self.amount = amount
self.amount_orig = amount_orig
if self.amount_orig > 0:
self.amount_filled = amount_orig - amount
else:
self.amount_filled = -(abs(amount_orig) - abs(amount))
self.type = o_type
self.type_prev = typePrev
self.flags = flags
self.status = status
self.price = price
self.price_avg = price_avg
self.price_trailing = price_trailing
self.price_aux_limit = price_aux_limit
self.notfiy = notfiy
self.place_id = place_id
self.tag = ""
self.fee = 0
self.is_pending_bool = True
self.is_confirmed_bool = False
self.is_open_bool = False
self.meta = meta or {}
self.date = datetime.datetime.fromtimestamp(mts_create/1000.0)
# if cancelled then priceAvg wont exist
if price_avg:
# check if order is taker or maker
if self.type in LIMIT_ORDERS:
self.fee = (price_avg * abs(self.amount_filled)) * 0.001
else:
self.fee = (price_avg * abs(self.amount_filled)) * 0.002
@staticmethod
def from_raw_order(raw_order):
"""
Parse a raw order object into an Order object
@return Order
"""
oid = raw_order[OrderClosedModel.ID]
gid = raw_order[OrderClosedModel.GID]
cid = raw_order[OrderClosedModel.CID]
symbol = raw_order[OrderClosedModel.SYMBOL]
mts_create = raw_order[OrderClosedModel.MTS_CREATE]
mts_update = raw_order[OrderClosedModel.MTS_UPDATE]
amount = raw_order[OrderClosedModel.AMOUNT]
amount_orig = raw_order[OrderClosedModel.AMOUNT_ORIG]
o_type = raw_order[OrderClosedModel.TYPE]
type_prev = raw_order[OrderClosedModel.TYPE_PREV]
flags = raw_order[OrderClosedModel.FLAGS]
status = raw_order[OrderClosedModel.STATUS]
price = raw_order[OrderClosedModel.PRICE]
price_avg = raw_order[OrderClosedModel.PRICE_AVG]
price_trailing = raw_order[OrderClosedModel.PRICE_TRAILING]
price_aux_limit = raw_order[OrderClosedModel.PRICE_AUX_LIMIT]
notfiy = raw_order[OrderClosedModel.NOTIFY]
place_id = raw_order[OrderClosedModel.PLACE_ID]
meta = raw_order[OrderClosedModel.META] or {}
return Order(oid, gid, cid, symbol, mts_create, mts_update, amount,
amount_orig, o_type, type_prev, flags, status, price, price_avg,
price_trailing, price_aux_limit, notfiy, place_id, meta)
@staticmethod
def from_raw_order_snapshot(raw_order_snapshot):
"""
Parse a raw order snapshot array into an array of order objects
@return Orders: array of order objects
"""
parsed_orders = []
for raw_order in raw_order_snapshot:
parsed_orders += [Order.from_raw_order(raw_order)]
return parsed_orders
def set_confirmed(self):
"""
Set the state of the order to be confirmed
"""
self.is_pending_bool = False
self.is_confirmed_bool = True
def set_open_state(self, is_open):
"""
Set the is_open state of the order
"""
self.is_open_bool = is_open
def is_open(self):
"""
Check if the order is still open
@return bool: True if order open else False
"""
return self.is_open_bool
def is_pending(self):
"""
Check if the state of the order is still pending
@return bool: True if is pending else False
"""
return self.is_pending_bool
def is_confirmed(self):
"""
Check if the order has been confirmed by the bitfinex api
@return bool: True if has been confirmed else False
"""
return self.is_confirmed_bool
def __str__(self):
"""
Allow us to print the Order object in a pretty format
"""
text = "Order <'{}' amount_orig={} amount_filled={} mts_create={} status='{}' id={}>"
return text.format(self.symbol, self.amount_orig, self.amount_filled,
self.mts_create, self.status, self.id)

View File

@@ -1,124 +0,0 @@
"""
Module used to describe all of the different data types
"""
import zlib
import json
class OrderBook:
"""
Object used to store the state of the orderbook. This can then be used
in one of two ways. To get the checksum of the book or so get the bids/asks
of the book
"""
def __init__(self):
self.asks = []
self.bids = []
def get_bids(self):
"""
Get all of the bids from the orderbook
@return bids Array
"""
return self.bids
def get_asks(self):
"""
Get all of the asks from the orderbook
@return asks Array
"""
return self.asks
def update_from_snapshot(self, data, orig_raw_msg):
"""
Update the orderbook with a raw orderbook snapshot
"""
# we need to keep the original string values that are sent to use
# this avoids any problems with floats
orig_raw = json.loads(orig_raw_msg, parse_float=str, parse_int=str)[1]
zip_data = []
# zip both the float values and string values together
for index, order in enumerate(data):
zip_data += [(order, orig_raw[index])]
## build our bids and asks
for order in zip_data:
if len(order[0]) == 4:
if order[0][3] < 0:
self.bids += [order]
else:
self.asks += [order]
else:
if order[0][2] < 0:
self.asks += [order]
else:
self.bids += [order]
def update_with(self, order, orig_raw_msg):
"""
Update the orderbook with a single update
"""
# keep orginal string vlues to avoid checksum float errors
orig_raw = json.loads(orig_raw_msg, parse_float=str, parse_int=str)[1]
zip_order = (order, orig_raw)
if len(order) == 4:
amount = order[3]
count = order[2]
side = self.bids if amount < 0 else self.asks
else:
amount = order[2]
side = self.asks if amount < 0 else self.bids
count = order[1]
price = order[0]
# if first item in ordebook
if len(side) == 0:
side += [zip_order]
return
# match price level but use the float parsed object
for index, s_order in enumerate(side):
s_price = s_order[0][0]
if s_price == price:
if count == 0:
del side[index]
return
# remove but add as new below
del side[index]
# if ob is initialised w/o all price levels
if count == 0:
return
# add to book and sort lowest to highest
side += [zip_order]
side.sort(key=lambda x: x[0][0], reverse=not amount < 0)
return
def checksum(self):
"""
Generate a CRC32 checksum of the orderbook
"""
data = []
# take set of top 25 bids/asks
for index in range(0, 25):
if index < len(self.bids):
# use the string parsed array
bid = self.bids[index][1]
price = bid[0]
amount = bid[3] if len(bid) == 4 else bid[2]
data += [price]
data += [amount]
if index < len(self.asks):
# use the string parsed array
ask = self.asks[index][1]
price = ask[0]
amount = ask[3] if len(ask) == 4 else ask[2]
data += [price]
data += [amount]
checksum_str = ':'.join(data)
# calculate checksum and force signed integer
checksum = zlib.crc32(checksum_str.encode('utf8')) & 0xffffffff
return checksum

View File

@@ -1,108 +0,0 @@
"""
Module used to describe all of the different data types
"""
class PositionModel:
"""
Enum used to index the different values in a raw position array
"""
SYMBOL = 0
STATUS = 1
AMOUNT = 2
BASE_PRICE = 3
MARGIN_FUNDING = 4
MARGIN_FUNDING_TYPE = 5
PL = 6
PL_PERC = 7
PRICE_LIQ = 8
LEVERAGE = 9
# _PLACEHOLDER,
POSITION_ID = 11
MTS_CREATE = 12
MTS_UPDATE = 13
# _PLACEHOLDER
TYPE = 15
# _PLACEHOLDER,
COLLATERAL = 17
COLLATERAL_MIN = 18
META = 19
class Position:
"""
SYMBOL string Pair (tBTCUSD, …).
STATUS string Status (ACTIVE, CLOSED).
±AMOUNT float Size of the position. A positive value indicates a
long position; a negative value indicates a short position.
BASE_PRICE float Base price of the position. (Average traded price
of the previous orders of the position)
MARGIN_FUNDING float The amount of funding being used for this position.
MARGIN_FUNDING_TYPE int 0 for daily, 1 for term.
PL float Profit & Loss
PL_PERC float Profit & Loss Percentage
PRICE_LIQ float Liquidation price
LEVERAGE float Leverage used for the position
POSITION_ID int64 Position ID
MTS_CREATE int Millisecond timestamp of creation
MTS_UPDATE int Millisecond timestamp of update
TYPE int Identifies the type of position, 0 = Margin position,
1 = Derivatives position
COLLATERAL float The amount of collateral applied to the open position
COLLATERAL_MIN float The minimum amount of collateral required for the position
META json string Additional meta information about the position
"""
def __init__(self, symbol, status, amount, b_price, m_funding, m_funding_type,
profit_loss, profit_loss_perc, l_price, lev, pid, mts_create, mts_update,
p_type, collateral, collateral_min, meta):
self.symbol = symbol
self.status = status
self.amount = amount
self.base_price = b_price
self.margin_funding = m_funding
self.margin_funding_type = m_funding_type
self.profit_loss = profit_loss
self.profit_loss_percentage = profit_loss_perc
self.liquidation_price = l_price
self.leverage = lev
self.id = pid
self.mts_create = mts_create
self.mts_update = mts_update
self.type = p_type
self.collateral = collateral
self.collateral_min = collateral_min
self.meta = meta
@staticmethod
def from_raw_rest_position(raw_position):
"""
Generate a Position object from a raw position array
@return Position
"""
sym = raw_position[PositionModel.SYMBOL]
status = raw_position[PositionModel.STATUS]
amnt = raw_position[PositionModel.AMOUNT]
b_price = raw_position[PositionModel.BASE_PRICE]
m_fund = raw_position[PositionModel.MARGIN_FUNDING]
m_fund_t = raw_position[PositionModel.MARGIN_FUNDING_TYPE]
pl = raw_position[PositionModel.PL]
pl_prc = raw_position[PositionModel.PL_PERC]
l_price = raw_position[PositionModel.PRICE_LIQ]
lev = raw_position[PositionModel.LEVERAGE]
pid = raw_position[PositionModel.POSITION_ID]
mtsc = raw_position[PositionModel.MTS_CREATE]
mtsu = raw_position[PositionModel.MTS_UPDATE]
ptype = raw_position[PositionModel.TYPE]
coll = raw_position[PositionModel.COLLATERAL]
coll_min = raw_position[PositionModel.COLLATERAL_MIN]
meta = raw_position[PositionModel.META]
return Position(sym, status, amnt, b_price, m_fund, m_fund_t, pl, pl_prc, l_price,
lev, pid, mtsc, mtsu, ptype, coll, coll_min, meta)
def __str__(self):
"""
Allow us to print the Trade object in a pretty format
"""
text = "Position '{}' {} x {} <status='{}' pl={}>"
return text.format(self.symbol, self.base_price, self.amount,
self.status, self.profit_loss)

View File

@@ -1,88 +0,0 @@
"""
Module used to describe all of the different data types
"""
import time
import json
from random import randint
def generate_sub_id():
"""
Generates a unique id in the form of 12345566-12334556
"""
prefix = str(int(round(time.time() * 1000)))
suffix = str(randint(0, 9999999))
return "{}-{}".format(prefix, suffix)
class Subscription:
"""
Object used to represent an individual subscription to the websocket.
This class also exposes certain functions which helps to manage the subscription
such as unsubscribe and subscribe.
"""
def __init__(self, socket, channel_name, symbol, key=None, timeframe=None, **kwargs):
self.socket = socket
self.channel_name = channel_name
self.symbol = symbol
self.timeframe = timeframe
self.is_subscribed_bool = False
self.key = key
self.chan_id = None
if timeframe:
self.key = 'trade:{}:{}'.format(self.timeframe, self.symbol)
self.sub_id = generate_sub_id()
self.send_payload = self._generate_payload(**kwargs)
def get_key(self):
"""
Generates a unique key string for the subscription
"""
return "{}_{}".format(self.channel_name, self.key or self.symbol)
def confirm_subscription(self, chan_id):
"""
Update the subscription to confirmed state
"""
self.is_subscribed_bool = True
self.chan_id = chan_id
async def unsubscribe(self):
"""
Send an un-subscription request to the bitfinex socket
"""
if not self.is_subscribed():
raise Exception("Subscription is not subscribed to websocket")
payload = {'event': 'unsubscribe', 'chanId': self.chan_id}
await self.socket.send(json.dumps(payload))
async def subscribe(self):
"""
Send a subscription request to the bitfinex socket
"""
await self.socket.send(json.dumps(self._get_send_payload()))
def confirm_unsubscribe(self):
"""
Update the subscription to unsubscribed state
"""
self.is_subscribed_bool = False
def is_subscribed(self):
"""
Check if the subscription is currently subscribed
@return bool: True if subscribed else False
"""
return self.is_subscribed_bool
def _generate_payload(self, **kwargs):
payload = {'event': 'subscribe',
'channel': self.channel_name, 'symbol': self.symbol}
if self.timeframe or self.key:
payload['key'] = self.key
payload.update(**kwargs)
return payload
def _get_send_payload(self):
return self.send_payload

View File

@@ -1,72 +0,0 @@
"""
Module used to describe all of the different data types
"""
class TickerModel:
"""
Enum used to index the different values in a raw ticker array
"""
BID = 0
BID_SIZE = 1
ASK = 2
ASK_SIZE = 3
DAILY_CHANGE = 4
DAILY_CHANGE_PERCENT = 5
LAST_PRICE = 6
VOLUME = 7
HIGH = 8
LOW = 9
class Ticker:
"""
BID float Price of last highest bid
BID_SIZE float Sum of the 25 highest bid sizes
ASK float Price of last lowest ask
ASK_SIZE float Sum of the 25 lowest ask sizes
DAILY_CHANGE float Amount that the last price has changed since yesterday
DAILY_CHANGE_PERCENT float Relative price change since yesterday (*100 for percentage change)
LAST_PRICE float Price of the last trade
VOLUME float Daily volume
HIGH float Daily high
LOW float Daily low
"""
def __init__(self, pair, bid, bid_size, ask, ask_size, daily_change, daily_change_rel,
last_price, volume, high, low):
self.pair = pair
self.bid = bid
self.bid_size = bid_size
self.ask = ask
self.ask_size = ask_size
self.daily_change = daily_change
self.daily_change_rel = daily_change_rel
self.last_price = last_price
self.volume = volume
self.high = high
self.low = low
@staticmethod
def from_raw_ticker(raw_ticker, pair):
"""
Generate a Ticker object from a raw ticker array
"""
# [72128,[6914.5,28.123061460000002,6914.6,22.472037289999996,175.8,0.0261,6915.7,
# 6167.26141685,6964.2,6710.8]]
return Ticker(
pair,
raw_ticker[TickerModel.BID],
raw_ticker[TickerModel.BID_SIZE],
raw_ticker[TickerModel.ASK],
raw_ticker[TickerModel.ASK_SIZE],
raw_ticker[TickerModel.DAILY_CHANGE],
raw_ticker[TickerModel.DAILY_CHANGE_PERCENT],
raw_ticker[TickerModel.LAST_PRICE],
raw_ticker[TickerModel.VOLUME],
raw_ticker[TickerModel.HIGH],
raw_ticker[TickerModel.LOW],
)
def __str__(self):
return "Ticker '{}' <last='{}' volume={}>".format(
self.pair, self.last_price, self.volume)

View File

@@ -1,81 +0,0 @@
"""
Module used to describe all of the different data types
"""
import datetime
class TradeModel:
"""
Enum used to index the different values in a raw trade array
"""
ID = 0
PAIR = 1
MTS_CREATE = 2
ORDER_ID = 3
EXEC_AMOUNT = 4
EXEC_PRICE = 5
ORDER_TYPE = 6
ORDER_PRICE = 7
MAKER = 8
FEE = 9
FEE_CURRENCY = 10
class Trade:
"""
ID integer Trade database id
PAIR string Pair (BTCUSD, ...)
MTS_CREATE integer Execution timestamp
ORDER_ID integer Order id
EXEC_AMOUNT float Positive means buy, negative means sell
EXEC_PRICE float Execution price
ORDER_TYPE string Order type
ORDER_PRICE float Order price
MAKER int 1 if true, 0 if false
FEE float Fee
FEE_CURRENCY string Fee currency
"""
SHORT = 'SHORT'
LONG = 'LONG'
def __init__(self, tid, pair, mts_create, order_id, amount, price, order_type,
order_price, maker, fee, fee_currency):
# pylint: disable=invalid-name
self.id = tid
self.pair = pair
self.mts_create = mts_create
self.date = datetime.datetime.fromtimestamp(mts_create/1000.0)
self.order_id = order_id
self.amount = amount
self.direction = Trade.SHORT if amount < 0 else Trade.LONG
self.price = price
self.order_type = order_type
self.order_price = order_price
self.maker = maker
self.fee = fee
self.fee_currency = fee_currency
@staticmethod
def from_raw_rest_trade(raw_trade):
"""
Generate a Trade object from a raw trade array
"""
# [24224048, 'tBTCUSD', 1542800024000, 1151353484, 0.09399997, 19963, None, None,
# -1, -0.000188, 'BTC']
tid = raw_trade[TradeModel.ID]
pair = raw_trade[TradeModel.PAIR]
mtsc = raw_trade[TradeModel.MTS_CREATE]
oid = raw_trade[TradeModel.ORDER_ID]
amnt = raw_trade[TradeModel.EXEC_AMOUNT]
price = raw_trade[TradeModel.EXEC_PRICE]
otype = raw_trade[TradeModel.ORDER_TYPE]
oprice = raw_trade[TradeModel.ORDER_PRICE]
maker = raw_trade[TradeModel.MAKER]
fee = raw_trade[TradeModel.FEE]
feeccy = raw_trade[TradeModel.FEE_CURRENCY]
return Trade(tid, pair, mtsc, oid, amnt, price, otype, oprice, maker,
fee, feeccy)
def __str__(self):
return "Trade '{}' x {} @ {} <direction='{}' fee={}>".format(
self.pair, self.amount, self.price, self.direction, self.fee)

View File

@@ -1,55 +0,0 @@
"""
Module used to describe a transfer object
"""
class TransferModel:
"""
Enum used to index the location of each value in a raw array
"""
MTS = 0
W_FROM = 1
W_TO = 2
C_FROM = 4
C_TO = 5
AMOUNT = 7
class Transfer:
"""
MTS int Millisecond Time Stamp of the update
WALLET_FROM string Wallet name (exchange, margin, funding)
WALLET_TO string Wallet name (exchange, margin, funding)
CURRENCY_FROM string Currency (BTC, etc)
CURRENCY_TO string Currency (BTC, etc)
AMOUNT string Amount of funds to transfer
"""
def __init__(self, mts, wallet_from, wallet_to, currency_from, currency_to, amount):
self.mts = mts
self.wallet_from = wallet_from
self.wallet_to = wallet_to
self.currency_from = currency_from
self.currency_to = currency_to
self.amount = amount
@staticmethod
def from_raw_transfer(raw_transfer):
"""
Parse a raw transfer object into a Transfer object
@return Transfer
"""
mts = raw_transfer[TransferModel.MTS]
wallet_from = raw_transfer[TransferModel.W_FROM]
wallet_to = raw_transfer[TransferModel.W_TO]
currency_from = raw_transfer[TransferModel.C_FROM]
currency_to = raw_transfer[TransferModel.C_TO]
amount = raw_transfer[TransferModel.AMOUNT]
return Transfer(mts, wallet_from, wallet_to, currency_from, currency_to, amount)
def __str__(self):
"""
Allow us to print the Transfer object in a pretty format
"""
text = "Transfer <{} from {} ({}) to {} ({}) mts={}>"
return text.format(self.amount, self.wallet_from, self.currency_from,
self.wallet_to, self.currency_to, self.mts)

View File

@@ -1,34 +0,0 @@
"""
Module used to describe all of the different data types
"""
class Wallet:
"""
Stores data relevant to a users wallet such as balance and
currency
"""
def __init__(self, wType, currency, balance, unsettled_interest, balance_available):
self.type = wType
self.currency = currency
self.balance = balance
self.balance_available = balance_available
self.unsettled_interest = unsettled_interest
self.key = "{}_{}".format(wType, currency)
def set_balance(self, data):
"""
Set the balance of the wallet
"""
self.balance = data
def set_unsettled_interest(self, data):
"""
Set the unsettled interest of the wallet
"""
self.unsettled_interest = data
def __str__(self):
return "Wallet <'{}_{}' balance='{}' balance_available='{}' unsettled='{}'>".format(
self.type, self.currency, self.balance, self.balance_available, self.unsettled_interest)

View File

@@ -1,54 +0,0 @@
"""
Module used to describe a withdraw object
"""
class WithdrawModel:
"""
Enum used to index the location of each value in a raw array
"""
ID = 0
METHOD = 2
WALLET = 4
AMOUNT = 5
FEE = 8
class Withdraw:
"""
[13063236, None, 'tetheruse', None, 'exchange', 5, None, None, 0.00135]
MTS int Millisecond Time Stamp of the update
WALLET_FROM string Wallet name (exchange, margin, funding)
WALLET_TO string Wallet name (exchange, margin, funding)
CURRENCY_FROM string Currency (BTC, etc)
CURRENCY_TO string Currency (BTC, etc)
AMOUNT string Amount of funds to transfer
"""
def __init__(self, w_id, method, wallet, amount, fee=0):
self.id = w_id
self.method = method
self.wallet = wallet
self.amount = amount
self.fee = fee
@staticmethod
def from_raw_withdraw(raw_withdraw):
"""
Parse a raw withdraw object into a Withdraw object
@return Withdraw
"""
w_id = raw_withdraw[WithdrawModel.ID]
method = raw_withdraw[WithdrawModel.METHOD]
wallet = raw_withdraw[WithdrawModel.WALLET]
amount = raw_withdraw[WithdrawModel.AMOUNT]
fee = raw_withdraw[WithdrawModel.FEE]
return Withdraw(w_id, method, wallet, amount, fee)
def __str__(self):
"""
Allow us to print the Withdraw object in a pretty format
"""
text = "Withdraw <id={} from {} ({}) amount={} fee={}>"
return text.format(self.id, self.wallet, self.method, self.amount,
self.fee)

View File

@@ -1 +1,2 @@
NAME = 'rest'
from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \
RestMerchantEndpoints

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
from .bfx_rest_interface import BfxRestInterface
from .rest_public_endpoints import RestPublicEndpoints
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints

View File

@@ -0,0 +1,13 @@
from .rest_public_endpoints import RestPublicEndpoints
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints
class BfxRestInterface:
VERSION = 2
def __init__(self, host, credentials = None):
api_key, api_secret = (credentials['api_key'], credentials['api_secret']) if credentials else (None, None)
self.public = RestPublicEndpoints(host=host)
self.auth = RestAuthenticatedEndpoints(host=host, api_key=api_key, api_secret=api_secret)
self.merchant = RestMerchantEndpoints(host=host, api_key=api_key, api_secret=api_secret)

View File

@@ -0,0 +1,474 @@
from typing import Dict, List, Tuple, Union, Literal, Optional
from decimal import Decimal
from datetime import datetime
from ..middleware import Middleware
from ..enums import Sort, OrderType, FundingOfferType
from ...types import JSON, Notification, \
UserInfo, LoginHistory, BalanceAvailable, \
Order, Position, Trade, \
FundingTrade, OrderTrade, Ledger, \
FundingOffer, FundingCredit, FundingLoan, \
FundingAutoRenew, FundingInfo, Wallet, \
Transfer, Withdrawal, DepositAddress, \
LightningNetworkInvoice, Movement, SymbolMarginInfo, \
BaseMarginInfo, PositionClaim, PositionIncreaseInfo, \
PositionIncrease, PositionHistory, PositionSnapshot, \
PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits
from ...types import serializers
from ...types.serializers import _Notification
class RestAuthenticatedEndpoints(Middleware):
def get_user_info(self) -> UserInfo:
return serializers.UserInfo \
.parse(*self._post("auth/r/info/user"))
def get_login_history(self) -> List[LoginHistory]:
return [ serializers.LoginHistory.parse(*sub_data)
for sub_data in self._post("auth/r/logins/hist") ]
def get_balance_available_for_orders_or_offers(self,
symbol: str,
type: str,
*,
dir: Optional[int] = None,
rate: Optional[str] = None,
lev: Optional[str] = None) -> BalanceAvailable:
body = {
"symbol": symbol, "type": type, "dir": dir,
"rate": rate, "lev": lev
}
return serializers.BalanceAvailable \
.parse(*self._post("auth/calc/order/avail", body=body))
def get_wallets(self) -> List[Wallet]:
return [ serializers.Wallet.parse(*sub_data) \
for sub_data in self._post("auth/r/wallets") ]
def get_orders(self,
*,
symbol: Optional[str] = None,
ids: Optional[List[str]] = None) -> List[Order]:
if symbol is None:
endpoint = "auth/r/orders"
else: endpoint = f"auth/r/orders/{symbol}"
return [ serializers.Order.parse(*sub_data) \
for sub_data in self._post(endpoint, body={ "id": ids }) ]
def submit_order(self,
type: OrderType,
symbol: str,
amount: Union[Decimal, float, str],
*,
price: Optional[Union[Decimal, float, str]] = None,
lev: Optional[int] = None,
price_trailing: Optional[Union[Decimal, float, str]] = None,
price_aux_limit: Optional[Union[Decimal, float, str]] = None,
price_oco_stop: Optional[Union[Decimal, float, str]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = 0,
tif: Optional[Union[datetime, str]] = None,
meta: Optional[JSON] = None) -> Notification[Order]:
body = {
"type": type, "symbol": symbol, "amount": amount,
"price": price, "lev": lev, "price_trailing": price_trailing,
"price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid,
"cid": cid, "flags": flags, "tif": tif,
"meta": meta
}
return _Notification[Order](serializers.Order) \
.parse(*self._post("auth/w/order/submit", body=body))
def update_order(self,
id: int,
*,
amount: Optional[Union[Decimal, float, str]] = None,
price: Optional[Union[Decimal, float, str]] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
gid: Optional[int] = None,
flags: Optional[int] = 0,
lev: Optional[int] = None,
delta: Optional[Union[Decimal, float, str]] = None,
price_aux_limit: Optional[Union[Decimal, float, str]] = None,
price_trailing: Optional[Union[Decimal, float, str]] = None,
tif: Optional[Union[datetime, str]] = None) -> Notification[Order]:
body = {
"id": id, "amount": amount, "price": price,
"cid": cid, "cid_date": cid_date, "gid": gid,
"flags": flags, "lev": lev, "delta": delta,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif
}
return _Notification[Order](serializers.Order) \
.parse(*self._post("auth/w/order/update", body=body))
def cancel_order(self,
*,
id: Optional[int] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None) -> Notification[Order]:
return _Notification[Order](serializers.Order) \
.parse(*self._post("auth/w/order/cancel", \
body={ "id": id, "cid": cid, "cid_date": cid_date }))
def cancel_order_multi(self,
*,
ids: Optional[List[int]] = None,
cids: Optional[List[Tuple[int, str]]] = None,
gids: Optional[List[int]] = None,
all: bool = False) -> Notification[List[Order]]:
body = {
"ids": ids, "cids": cids, "gids": gids,
"all": int(all)
}
return _Notification[List[Order]](serializers.Order, is_iterable=True) \
.parse(*self._post("auth/w/order/cancel/multi", body=body))
def get_orders_history(self,
*,
symbol: Optional[str] = None,
ids: Optional[List[int]] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Order]:
if symbol is None:
endpoint = "auth/r/orders/hist"
else: endpoint = f"auth/r/orders/{symbol}/hist"
body = {
"id": ids, "start": start, "end": end,
"limit": limit
}
return [ serializers.Order.parse(*sub_data) \
for sub_data in self._post(endpoint, body=body) ]
def get_order_trades(self,
symbol: str,
id: int) -> List[OrderTrade]:
return [ serializers.OrderTrade.parse(*sub_data) \
for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades") ]
def get_trades_history(self,
*,
symbol: Optional[str] = None,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Trade]:
if symbol is None:
endpoint = "auth/r/trades/hist"
else: endpoint = f"auth/r/trades/{symbol}/hist"
body = {
"sort": sort, "start": start, "end": end,
"limit": limit
}
return [ serializers.Trade.parse(*sub_data) \
for sub_data in self._post(endpoint, body=body) ]
def get_ledgers(self,
currency: str,
*,
category: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Ledger]:
body = {
"category": category, "start": start, "end": end,
"limit": limit
}
return [ serializers.Ledger.parse(*sub_data) \
for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) ]
def get_base_margin_info(self) -> BaseMarginInfo:
return serializers.BaseMarginInfo \
.parse(*(self._post("auth/r/info/margin/base")[1]))
def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo:
return serializers.SymbolMarginInfo \
.parse(*self._post(f"auth/r/info/margin/{symbol}"))
def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]:
return [ serializers.SymbolMarginInfo.parse(*sub_data) \
for sub_data in self._post("auth/r/info/margin/sym_all") ]
def get_positions(self) -> List[Position]:
return [ serializers.Position.parse(*sub_data) \
for sub_data in self._post("auth/r/positions") ]
def claim_position(self,
id: int,
*,
amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]:
return _Notification[PositionClaim](serializers.PositionClaim) \
.parse(*self._post("auth/w/position/claim", \
body={ "id": id, "amount": amount }))
def increase_position(self,
symbol: str,
amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]:
return _Notification[PositionIncrease](serializers.PositionIncrease) \
.parse(*self._post("auth/w/position/increase", \
body={ "symbol": symbol, "amount": amount }))
def get_increase_position_info(self,
symbol: str,
amount: Union[Decimal, float, str]) -> PositionIncreaseInfo:
return serializers.PositionIncreaseInfo \
.parse(*self._post("auth/r/position/increase/info", \
body={ "symbol": symbol, "amount": amount }))
def get_positions_history(self,
*,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[PositionHistory]:
return [ serializers.PositionHistory.parse(*sub_data) \
for sub_data in self._post("auth/r/positions/hist", \
body={ "start": start, "end": end, "limit": limit }) ]
def get_positions_snapshot(self,
*,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[PositionSnapshot]:
return [ serializers.PositionSnapshot.parse(*sub_data) \
for sub_data in self._post("auth/r/positions/snap", \
body={ "start": start, "end": end, "limit": limit }) ]
def get_positions_audit(self,
*,
ids: Optional[List[int]] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[PositionAudit]:
body = {
"ids": ids, "start": start, "end": end,
"limit": limit
}
return [ serializers.PositionAudit.parse(*sub_data) \
for sub_data in self._post("auth/r/positions/audit", body=body) ]
def set_derivative_position_collateral(self,
symbol: str,
collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral:
return serializers.DerivativePositionCollateral \
.parse(*(self._post("auth/w/deriv/collateral/set", \
body={ "symbol": symbol, "collateral": collateral })[0]))
def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits:
return serializers.DerivativePositionCollateralLimits \
.parse(*self._post("auth/calc/deriv/collateral/limit", body={ "symbol": symbol }))
def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOffer]:
if symbol is None:
endpoint = "auth/r/funding/offers"
else: endpoint = f"auth/r/funding/offers/{symbol}"
return [ serializers.FundingOffer.parse(*sub_data) \
for sub_data in self._post(endpoint) ]
#pylint: disable-next=too-many-arguments
def submit_funding_offer(self,
type: FundingOfferType,
symbol: str,
amount: Union[Decimal, float, str],
rate: Union[Decimal, float, str],
period: int,
*,
flags: Optional[int] = 0) -> Notification[FundingOffer]:
body = {
"type": type, "symbol": symbol, "amount": amount,
"rate": rate, "period": period, "flags": flags
}
return _Notification[FundingOffer](serializers.FundingOffer) \
.parse(*self._post("auth/w/funding/offer/submit", body=body))
def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]:
return _Notification[FundingOffer](serializers.FundingOffer) \
.parse(*self._post("auth/w/funding/offer/cancel", body={ "id": id }))
def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]:
return _Notification[Literal[None]](None) \
.parse(*self._post("auth/w/funding/offer/cancel/all", body={ "currency": currency }))
def submit_funding_close(self, id: int) -> Notification[Literal[None]]:
return _Notification[Literal[None]](None) \
.parse(*self._post("auth/w/funding/close", body={ "id": id }))
def toggle_auto_renew(self,
status: bool,
currency: str,
*,
amount: Optional[str] = None,
rate: Optional[int] = None,
period: Optional[int] = None) -> Notification[FundingAutoRenew]:
body = {
"status": int(status), "currency": currency, "amount": amount,
"rate": rate, "period": period
}
return _Notification[FundingAutoRenew](serializers.FundingAutoRenew) \
.parse(*self._post("auth/w/funding/auto", body=body))
def toggle_keep_funding(self,
type: Literal["credit", "loan"],
*,
ids: Optional[List[int]] = None,
changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]:
return _Notification[Literal[None]](None) \
.parse(*self._post("auth/w/funding/keep", \
body={ "type": type, "id": ids, "changes": changes }))
def get_funding_offers_history(self,
*,
symbol: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingOffer]:
if symbol is None:
endpoint = "auth/r/funding/offers/hist"
else: endpoint = f"auth/r/funding/offers/{symbol}/hist"
return [ serializers.FundingOffer.parse(*sub_data) \
for sub_data in self._post(endpoint, \
body={ "start": start, "end": end, "limit": limit }) ]
def get_funding_loans(self, *, symbol: Optional[str] = None) -> List[FundingLoan]:
if symbol is None:
endpoint = "auth/r/funding/loans"
else: endpoint = f"auth/r/funding/loans/{symbol}"
return [ serializers.FundingLoan.parse(*sub_data) \
for sub_data in self._post(endpoint) ]
def get_funding_loans_history(self,
*,
symbol: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingLoan]:
if symbol is None:
endpoint = "auth/r/funding/loans/hist"
else: endpoint = f"auth/r/funding/loans/{symbol}/hist"
return [ serializers.FundingLoan.parse(*sub_data) \
for sub_data in self._post(endpoint, \
body={ "start": start, "end": end, "limit": limit }) ]
def get_funding_credits(self, *, symbol: Optional[str] = None) -> List[FundingCredit]:
if symbol is None:
endpoint = "auth/r/funding/credits"
else: endpoint = f"auth/r/funding/credits/{symbol}"
return [ serializers.FundingCredit.parse(*sub_data) \
for sub_data in self._post(endpoint) ]
def get_funding_credits_history(self,
*,
symbol: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingCredit]:
if symbol is None:
endpoint = "auth/r/funding/credits/hist"
else: endpoint = f"auth/r/funding/credits/{symbol}/hist"
return [ serializers.FundingCredit.parse(*sub_data) \
for sub_data in self._post(endpoint, \
body={ "start": start, "end": end, "limit": limit }) ]
def get_funding_trades_history(self,
*,
symbol: Optional[str] = None,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingTrade]:
if symbol is None:
endpoint = "auth/r/funding/trades/hist"
else: endpoint = f"auth/r/funding/trades/{symbol}/hist"
body = {
"sort": sort, "start": start, "end": end,
"limit": limit }
return [ serializers.FundingTrade.parse(*sub_data) \
for sub_data in self._post(endpoint, body=body) ]
def get_funding_info(self, key: str) -> FundingInfo:
return serializers.FundingInfo \
.parse(*(self._post(f"auth/r/info/funding/{key}")[2]))
#pylint: disable-next=too-many-arguments
def transfer_between_wallets(self,
from_wallet: str,
to_wallet: str,
currency: str,
currency_to: str,
amount: Union[Decimal, float, str]) -> Notification[Transfer]:
body = {
"from": from_wallet, "to": to_wallet, "currency": currency,
"currency_to": currency_to, "amount": amount
}
return _Notification[Transfer](serializers.Transfer) \
.parse(*self._post("auth/w/transfer", body=body))
def submit_wallet_withdrawal(self,
wallet: str,
method: str,
address: str,
amount: Union[Decimal, float, str]) -> Notification[Withdrawal]:
body = {
"wallet": wallet, "method": method, "address": address,
"amount": amount
}
return _Notification[Withdrawal](serializers.Withdrawal) \
.parse(*self._post("auth/w/withdraw", body=body))
def get_deposit_address(self,
wallet: str,
method: str,
renew: bool = False) -> Notification[DepositAddress]:
return _Notification[DepositAddress](serializers.DepositAddress) \
.parse(*self._post("auth/w/deposit/address", \
body={ "wallet": wallet, "method": method, "renew": int(renew) }))
def generate_deposit_invoice(self,
wallet: str,
currency: str,
amount: Union[Decimal, float, str]) -> LightningNetworkInvoice:
return serializers.LightningNetworkInvoice \
.parse(*self._post("auth/w/deposit/invoice", \
body={ "wallet": wallet, "currency": currency, "amount": amount }))
def get_movements(self,
*,
currency: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Movement]:
if currency is None:
endpoint = "auth/r/movements/hist"
else: endpoint = f"auth/r/movements/{currency}/hist"
return [ serializers.Movement.parse(*sub_data) \
for sub_data in self._post(endpoint, \
body={ "start": start, "end": end, "limit": limit }) ]

View File

@@ -0,0 +1,185 @@
import re
from typing import Callable, TypeVar, cast, \
TypedDict, Dict, List, Union, Literal, Optional, Any
from decimal import Decimal
from ..middleware import Middleware
from ..enums import MerchantSettingsKey
from ...types import \
InvoiceSubmission, InvoicePage, InvoiceStats, \
CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit
#region Defining methods to convert dictionary keys to snake_case and camelCase.
T = TypeVar("T")
_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(?<!^)(?=[A-Z])", "_", string).lower()
_to_camel_case: Callable[[str], str] = lambda string: \
(components := string.split("_"))[0] + str().join(c.title() for c in components[1:])
def _scheme(data: T, adapter: Callable[[str], str]) -> T:
if isinstance(data, list):
return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ])
if isinstance(data, dict):
return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() })
return data
def _to_snake_case_keys(dictionary: T) -> T:
return _scheme(dictionary, _to_snake_case)
def _to_camel_case_keys(dictionary: T) -> T:
return _scheme(dictionary, _to_camel_case)
#endregion
_CustomerInfo = TypedDict("_CustomerInfo", {
"nationality": str, "resid_country": str, "resid_city": str,
"resid_zip_code": str, "resid_street": str, "resid_building_no": str,
"full_name": str, "email": str, "tos_accepted": bool
})
class RestMerchantEndpoints(Middleware):
#pylint: disable-next=too-many-arguments
def submit_invoice(self,
amount: Union[Decimal, float, str],
currency: str,
order_id: str,
customer_info: _CustomerInfo,
pay_currencies: List[str],
*,
duration: Optional[int] = None,
webhook: Optional[str] = None,
redirect_url: Optional[str] = None) -> InvoiceSubmission:
body = _to_camel_case_keys({
"amount": amount, "currency": currency, "order_id": order_id,
"customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration,
"webhook": webhook, "redirect_url": redirect_url
})
data = _to_snake_case_keys(self._post("auth/w/ext/pay/invoice/create", body=body))
return InvoiceSubmission.parse(data)
def get_invoices(self,
*,
id: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[InvoiceSubmission]:
body = {
"id": id, "start": start, "end": end,
"limit": limit
}
response = self._post("auth/r/ext/pay/invoices", body=body)
return [ InvoiceSubmission.parse(sub_data) for sub_data in _to_snake_case_keys(response) ]
def get_invoices_paginated(self,
page: int = 1,
page_size: int = 10,
sort: Literal["asc", "desc"] = "asc",
sort_field: Literal["t", "amount", "status"] = "t",
*,
status: Optional[List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]] = None,
fiat: Optional[List[str]] = None,
crypto: Optional[List[str]] = None,
id: Optional[str] = None,
order_id: Optional[str] = None) -> InvoicePage:
body = _to_camel_case_keys({
"page": page, "page_size": page_size, "sort": sort,
"sort_field": sort_field, "status": status, "fiat": fiat,
"crypto": crypto, "id": id, "order_id": order_id
})
data = _to_snake_case_keys(self._post("auth/r/ext/pay/invoices/paginated", body=body))
return InvoicePage.parse(data)
def get_invoice_count_stats(self,
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"],
format: str) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in \
self._post("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ]
def get_invoice_earning_stats(self,
currency: str,
format: str) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in \
self._post("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ]
def complete_invoice(self,
id: str,
pay_currency: str,
*,
deposit_id: Optional[int] = None,
ledger_id: Optional[int] = None) -> InvoiceSubmission:
return InvoiceSubmission.parse(_to_snake_case_keys(self._post("auth/w/ext/pay/invoice/complete", body={
"id": id, "payCcy": pay_currency, "depositId": deposit_id,
"ledgerId": ledger_id
})))
def expire_invoice(self, id: str) -> InvoiceSubmission:
body = { "id": id }
response = self._post("auth/w/ext/pay/invoice/expire", body=body)
return InvoiceSubmission.parse(_to_snake_case_keys(response))
def get_currency_conversion_list(self) -> List[CurrencyConversion]:
return [
CurrencyConversion(
base_currency=sub_data["baseCcy"],
convert_currency=sub_data["convertCcy"],
created=sub_data["created"]
) for sub_data in self._post("auth/r/ext/pay/settings/convert/list")
]
def add_currency_conversion(self,
base_currency: str,
convert_currency: str) -> bool:
return bool(self._post("auth/w/ext/pay/settings/convert/create", body={
"baseCcy": base_currency,
"convertCcy": convert_currency
}))
def remove_currency_conversion(self,
base_currency: str,
convert_currency: str) -> bool:
return bool(self._post("auth/w/ext/pay/settings/convert/remove", body={
"baseCcy": base_currency,
"convertCcy": convert_currency
}))
def set_merchant_settings(self,
key: MerchantSettingsKey,
val: Any) -> bool:
return bool(self._post("auth/w/ext/pay/settings/set", body={ "key": key, "val": val }))
def get_merchant_settings(self, key: MerchantSettingsKey) -> Any:
return self._post("auth/r/ext/pay/settings/get", body={ "key": key })
def list_merchant_settings(self, keys: List[MerchantSettingsKey] = []) -> Dict[MerchantSettingsKey, Any]:
return self._post("auth/r/ext/pay/settings/list", body={ "keys": keys })
def get_deposits(self,
start: int,
end: int,
*,
ccy: Optional[str] = None,
unlinked: Optional[bool] = None) -> List[MerchantDeposit]:
body = { "from": start, "to": end, "ccy": ccy, "unlinked": unlinked }
response = self._post("auth/r/ext/pay/deposits", body=body)
return [ MerchantDeposit(**sub_data) for sub_data in _to_snake_case_keys(response) ]
def get_unlinked_deposits(self,
ccy: str,
*,
start: Optional[int] = None,
end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]:
body = { "ccy": ccy, "start": start, "end": end }
response = self._post("/auth/r/ext/pay/deposits/unlinked", body=body)
return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in _to_snake_case_keys(response) ]

View File

@@ -0,0 +1,294 @@
from typing import List, Dict, Union, Literal, Optional, Any, cast
from decimal import Decimal
from ..middleware import Middleware
from ..enums import Config, Sort
from ...types import \
PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \
TickersHistory, TradingPairTrade, FundingCurrencyTrade, \
TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \
FundingCurrencyRawBook, Statistic, Candle, \
DerivativesStatus, Liquidation, Leaderboard, \
FundingStatistic, PulseProfile, PulseMessage, \
TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate
from ...types import serializers
class RestPublicEndpoints(Middleware):
def conf(self, config: Config) -> Any:
return self._get(f"conf/{config}")[0]
def get_platform_status(self) -> PlatformStatus:
return serializers.PlatformStatus.parse(*self._get("platform/status"))
def get_tickers(self, symbols: List[str]) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]:
data = self._get("tickers", params={ "symbols": ",".join(symbols) })
parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }
return {
symbol: cast(Union[TradingPairTicker, FundingCurrencyTicker],
parsers[symbol[0]](*sub_data)) for sub_data in data
if (symbol := sub_data.pop(0))
}
def get_t_tickers(self, symbols: Union[List[str], Literal["ALL"]]) -> Dict[str, TradingPairTicker]:
if isinstance(symbols, str) and symbols == "ALL":
return {
symbol: cast(TradingPairTicker, sub_data)
for symbol, sub_data in self.get_tickers([ "ALL" ]).items()
if symbol.startswith("t")
}
data = self.get_tickers(list(symbols))
return cast(Dict[str, TradingPairTicker], data)
def get_f_tickers(self, symbols: Union[List[str], Literal["ALL"]]) -> Dict[str, FundingCurrencyTicker]:
if isinstance(symbols, str) and symbols == "ALL":
return {
symbol: cast(FundingCurrencyTicker, sub_data)
for symbol, sub_data in self.get_tickers([ "ALL" ]).items()
if symbol.startswith("f")
}
data = self.get_tickers(list(symbols))
return cast(Dict[str, FundingCurrencyTicker], data)
def get_t_ticker(self, symbol: str) -> TradingPairTicker:
return serializers.TradingPairTicker.parse(*self._get(f"ticker/{symbol}"))
def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker:
return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{symbol}"))
def get_tickers_history(self,
symbols: List[str],
*,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[TickersHistory]:
return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._get("tickers/hist", params={
"symbols": ",".join(symbols),
"start": start, "end": end,
"limit": limit
}) ]
def get_t_trades(self,
pair: str,
*,
limit: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
sort: Optional[Sort] = None) -> List[TradingPairTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
data = self._get(f"trades/{pair}/hist", params=params)
return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ]
def get_f_trades(self,
currency: str,
*,
limit: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
data = self._get(f"trades/{currency}/hist", params=params)
return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ]
def get_t_book(self,
pair: str,
precision: Literal["P0", "P1", "P2", "P3", "P4"],
*,
len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]:
return [ serializers.TradingPairBook.parse(*sub_data) \
for sub_data in self._get(f"book/{pair}/{precision}", params={ "len": len }) ]
def get_f_book(self,
currency: str,
precision: Literal["P0", "P1", "P2", "P3", "P4"],
*,
len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]:
return [ serializers.FundingCurrencyBook.parse(*sub_data) \
for sub_data in self._get(f"book/{currency}/{precision}", params={ "len": len }) ]
def get_t_raw_book(self,
pair: str,
*,
len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]:
return [ serializers.TradingPairRawBook.parse(*sub_data) \
for sub_data in self._get(f"book/{pair}/R0", params={ "len": len }) ]
def get_f_raw_book(self,
currency: str,
*,
len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]:
return [ serializers.FundingCurrencyRawBook.parse(*sub_data) \
for sub_data in self._get(f"book/{currency}/R0", params={ "len": len }) ]
def get_stats_hist(self,
resource: str,
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Statistic]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"stats1/{resource}/hist", params=params)
return [ serializers.Statistic.parse(*sub_data) for sub_data in data ]
def get_stats_last(self,
resource: str,
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> Statistic:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"stats1/{resource}/last", params=params)
return serializers.Statistic.parse(*data)
def get_candles_hist(self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Candle]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params)
return [ serializers.Candle.parse(*sub_data) for sub_data in data ]
def get_candles_last(self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> Candle:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"candles/trade:{tf}:{symbol}/last", params=params)
return serializers.Candle.parse(*data)
def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> Dict[str, DerivativesStatus]:
if keys == "ALL":
params = { "keys": "ALL" }
else: params = { "keys": ",".join(keys) }
data = self._get("status/deriv", params=params)
return {
key: serializers.DerivativesStatus.parse(*sub_data)
for sub_data in data
if (key := sub_data.pop(0))
}
def get_derivatives_status_history(self,
key: str,
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[DerivativesStatus]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"status/deriv/{key}/hist", params=params)
return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ]
def get_liquidations(self,
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Liquidation]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get("liquidations/hist", params=params)
return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ]
def get_seed_candles(self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Candle]:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params)
return [ serializers.Candle.parse(*sub_data) for sub_data in data ]
def get_leaderboards_hist(self,
resource: str,
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Leaderboard]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"rankings/{resource}/hist", params=params)
return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ]
def get_leaderboards_last(self,
resource: str,
*,
sort: Optional[Sort] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> Leaderboard:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._get(f"rankings/{resource}/last", params=params)
return serializers.Leaderboard.parse(*data)
def get_funding_stats(self,
symbol: str,
*,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingStatistic]:
params = { "start": start, "end": end, "limit": limit }
data = self._get(f"funding/stats/{symbol}/hist", params=params)
return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ]
def get_pulse_profile_details(self, nickname: str) -> PulseProfile:
return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}"))
def get_pulse_message_history(self,
*,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[PulseMessage]:
messages = []
for subdata in self._get("pulse/hist", params={ "end": end, "limit": limit }):
subdata[18] = subdata[18][0]
message = serializers.PulseMessage.parse(*subdata)
messages.append(message)
return messages
def get_trading_market_average_price(self,
symbol: str,
amount: Union[Decimal, float, str],
*,
price_limit: Optional[Union[Decimal, float, str]] = None
) -> TradingMarketAveragePrice:
return serializers.TradingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={
"symbol": symbol, "amount": amount, "price_limit": price_limit
}))
def get_funding_market_average_price(self,
symbol: str,
amount: Union[Decimal, float, str],
period: int,
*,
rate_limit: Optional[Union[Decimal, float, str]] = None
) -> FundingMarketAveragePrice:
return serializers.FundingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={
"symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit
}))
def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate:
return serializers.FxRate.parse(*self._post("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 }))

47
bfxapi/rest/enums.py Normal file
View File

@@ -0,0 +1,47 @@
#pylint: disable-next=wildcard-import,unused-wildcard-import
from ..enums import *
class Config(str, Enum):
MAP_CURRENCY_SYM = "pub:map:currency:sym"
MAP_CURRENCY_LABEL = "pub:map:currency:label"
MAP_CURRENCY_UNIT = "pub:map:currency:unit"
MAP_CURRENCY_UNDL = "pub:map:currency:undl"
MAP_CURRENCY_POOL = "pub:map:currency:pool"
MAP_CURRENCY_EXPLORER = "pub:map:currency:explorer"
MAP_CURRENCY_TX_FEE = "pub:map:currency:tx:fee"
MAP_TX_METHOD = "pub:map:tx:method"
LIST_PAIR_EXCHANGE = "pub:list:pair:exchange"
LIST_PAIR_MARGIN = "pub:list:pair:margin"
LIST_PAIR_FUTURES = "pub:list:pair:futures"
LIST_PAIR_SECURITIES = "pub:list:pair:securities"
LIST_CURRENCY = "pub:list:currency"
LIST_COMPETITIONS = "pub:list:competitions"
INFO_PAIR = "pub:info:pair"
INFO_PAIR_FUTURES = "pub:info:pair:futures"
INFO_TX_STATUS = "pub:info:tx:status"
SPEC_MARGIN = "pub:spec:margin"
FEES = "pub:fees"
class Precision(str, Enum):
P0 = "P0"
P1 = "P1"
P2 = "P2"
P3 = "P3"
P4 = "P4"
class Sort(int, Enum):
ASCENDING = +1
DESCENDING = -1
class MerchantSettingsKey(str, Enum):
PREFERRED_FIAT = "bfx_pay_preferred_fiat"
RECOMMEND_STORE = "bfx_pay_recommend_store"
NOTIFY_PAYMENT_COMPLETED = "bfx_pay_notify_payment_completed"
NOTIFY_PAYMENT_COMPLETED_EMAIL = "bfx_pay_notify_payment_completed_email"
NOTIFY_AUTOCONVERT_EXECUTED = "bfx_pay_notify_autoconvert_executed"
DUST_BALANCE_UI = "bfx_pay_dust_balance_ui"
MERCHANT_CUSTOMER_SUPPORT_URL = "bfx_pay_merchant_customer_support_url"
MERCHANT_UNDERPAID_THRESHOLD = "bfx_pay_merchant_underpaid_threshold"

35
bfxapi/rest/exceptions.py Normal file
View File

@@ -0,0 +1,35 @@
from ..exceptions import BfxBaseException
__all__ = [
"BfxRestException",
"ResourceNotFound",
"RequestParametersError",
"ResourceNotFound",
"InvalidAuthenticationCredentials"
]
class BfxRestException(BfxBaseException):
"""
Base class for all custom exceptions in bfxapi/rest/exceptions.py.
"""
class ResourceNotFound(BfxRestException):
"""
This error indicates a failed HTTP request to a non-existent resource.
"""
class RequestParametersError(BfxRestException):
"""
This error indicates that there are some invalid parameters sent along with an HTTP request.
"""
class InvalidAuthenticationCredentials(BfxRestException):
"""
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
"""
class UnknownGenericError(BfxRestException):
"""
This error indicates an undefined problem processing an HTTP request sent to the APIs.
"""

View File

@@ -0,0 +1 @@
from .middleware import Middleware

View File

@@ -0,0 +1,99 @@
from typing import TYPE_CHECKING, Optional, Any
from http import HTTPStatus
import time, hmac, hashlib, json, requests
from ..enums import Error
from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError
from ...utils.json_encoder import JSONEncoder
if TYPE_CHECKING:
from requests.sessions import _Params
class Middleware:
TIMEOUT = 30
def __init__(self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None):
self.host, self.api_key, self.api_secret = host, api_key, api_secret
def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None):
assert isinstance(self.api_key, str) and isinstance(self.api_secret, str), \
"API_KEY and API_SECRET must be both str to call __build_authentication_headers"
nonce = str(round(time.time() * 1_000_000))
if data is None:
path = f"/api/v2/{endpoint}{nonce}"
else: path = f"/api/v2/{endpoint}{nonce}{data}"
signature = hmac.new(
self.api_secret.encode("utf8"),
path.encode("utf8"),
hashlib.sha384
).hexdigest()
return {
"bfx-nonce": nonce,
"bfx-signature": signature,
"bfx-apikey": self.api_key
}
def _get(self, endpoint: str, params: Optional["_Params"] = None) -> Any:
response = requests.get(
url=f"{self.host}/{endpoint}",
params=params,
timeout=Middleware.TIMEOUT
)
if response.status_code == HTTPStatus.NOT_FOUND:
raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.")
data = response.json()
if len(data) and data[0] == "error":
if data[1] == Error.ERR_PARAMS:
raise RequestParametersError("The request was rejected with the " \
f"following parameter error: <{data[2]}>")
if data[1] is None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
raise UnknownGenericError("The server replied to the request with " \
f"a generic error with message: <{data[2]}>.")
return data
def _post(self, endpoint: str, params: Optional["_Params"] = None,
body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any:
data = body and json.dumps(body, cls=JSONEncoder) or None
headers = { "Content-Type": "application/json" }
if self.api_key and self.api_secret and not _ignore_authentication_headers:
headers = { **headers, **self.__build_authentication_headers(endpoint, data) }
response = requests.post(
url=f"{self.host}/{endpoint}",
params=params,
data=data,
headers=headers,
timeout=Middleware.TIMEOUT
)
if response.status_code == HTTPStatus.NOT_FOUND:
raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.")
data = response.json()
if isinstance(data, list) and len(data) and data[0] == "error":
if data[1] == Error.ERR_PARAMS:
raise RequestParametersError("The request was rejected with the " \
f"following parameter error: <{data[2]}>")
if data[1] == Error.ERR_AUTH_FAIL:
raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.")
if data[1] is None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
raise UnknownGenericError("The server replied to the request with " \
f"a generic error with message: <{data[2]}>.")
return data

View File

@@ -0,0 +1,15 @@
import unittest
from .test_types_labeler import TestTypesLabeler
from .test_types_notification import TestTypesNotification
from .test_types_serializers import TestTypesSerializers
def suite():
return unittest.TestSuite([
unittest.makeSuite(TestTypesLabeler),
unittest.makeSuite(TestTypesNotification),
unittest.makeSuite(TestTypesSerializers),
])
if __name__ == "__main__":
unittest.TextTestRunner().run(suite())

View File

@@ -1,114 +0,0 @@
import time
import json
import asyncio
from pyee import EventEmitter
from .. import Client, BfxWebsocket, Socket
def get_now():
return int(round(time.time() * 1000))
def ev_worker_override():
return EventEmitter()
class StubbedWebsocket(BfxWebsocket):
def __init__(self, *args, **kwargs):
self.sent_items = []
self.published_items = []
super().__init__(create_event_emitter=ev_worker_override, *args, **kwargs)
async def _main(self, host):
print ("Faking wesocket connection to {}".format(host))
def _start_new_socket(self):
socket = Socket(len(self.sockets))
socket.set_connected()
socket.ws = self
self.sockets[socket.id] = socket
return socket.id
def _wait_for_socket(self, socketId):
return
async def publish(self, data, is_json=True):
self.published_items += [{
'time': get_now(),
'data': data
}]
# convert to string and push through the websocket
data = json.dumps(data) if is_json else data
return await self.on_message(0, data)
async def publish_auth_confirmation(self):
return self.publish({"event":"auth","status":"OK","chanId":0,"userId":269499,"auth_id":"58aa0472-b1a9-4690-8ab8-300d68e66aaf","caps":{"orders":{"read":1,"write":1},"account":{"read":1,"write":0},"funding":{"read":1,"write":1},"history":{"read":1,"write":0},"wallets":{"read":1,"write":1},"withdraw":{"read":0,"write":1},"positions":{"read":1,"write":1}}})
async def send(self, data_string):
self.sent_items += [{
'time': get_now(),
'data': data_string
}]
def get_published_items(self):
return self.published_items
def get_sent_items(self):
return self.sent_items
def get_last_sent_item(self):
return self.sent_items[-1:][0]
def get_sent_items_count(self):
return len(self.sent_items)
class EventWatcher():
def __init__(self, ws, event):
self.value = None
self.event = event
ws.once(event, self._finish)
def _finish(self, value):
self.value = value or {}
@classmethod
def watch(cls, ws, event):
return EventWatcher(ws, event)
def wait_until_complete(self, max_wait_time=5):
counter = 0
while self.value == None:
if counter > 5:
raise Exception('Wait time limit exceeded for event {}'.format(self.event))
time.sleep(1)
counter += 1
return self.value
class StubClient():
ws = None
res = None
def create_stubbed_client(*args, **kwargs):
client = StubClient()
# no support for rest stubbing yet
client.rest = None
wsStub = StubbedWebsocket(*args, **kwargs)
# stub client.ws so tests can use publish
client.ws = wsStub
client.ws.API_KEY = "test key"
client.ws.API_SECRET = "secret key"
# stub socket so we can track socket send requests
socket = Socket(0)
socket.set_connected()
socket.ws = wsStub
client.ws.sockets = { 0: socket }
return client
async def ws_publish_auth_accepted(ws):
return await ws.publish({"event":"auth","status":"OK","chanId":0,"userId":269499,"auth_id":"58aa0472-b1a9-4690-8ab8-300d68e66aaf","caps":{"orders":{"read":1,"write":1},"account":{"read":1,"write":0},"funding":{"read":1,"write":1},"history":{"read":1,"write":0},"wallets":{"read":1,"write":1},"withdraw":{"read":0,"write":1},"positions":{"read":1,"write":1}}})
async def ws_publish_connection_init(ws):
return await ws.publish({"event":"info","version":2,"serverId":"748c00f2-250b-46bb-8519-ce1d7d68e4f0","platform":{"status":1}})
async def ws_publish_conf_accepted(ws, flags_code):
return await ws.publish({"event":"conf","status":"OK","flags":flags_code})

View File

@@ -1,24 +0,0 @@
import sys
sys.path.append('../components')
from bfxapi import Decimal
def test_precision():
assert str(Decimal(0.00000123456789)) == "0.00000123456789"
assert str(Decimal("0.00000123456789")) == "0.00000123456789"
def test_float_operations():
assert str(Decimal(0.0002) * 0.02) == "0.000004"
assert str(0.02 * Decimal(0.0002)) == "0.000004"
assert str(Decimal(0.0002) / 0.02) == "0.01"
assert str(0.02 / Decimal(0.0002)) == "0.01"
assert str(0.02 + Decimal(0.0002)) == "0.0202"
assert str(Decimal(0.0002) + 0.02) == "0.0202"
assert str(0.02 - Decimal(0.0002)) == "-0.0198"
assert str(Decimal(0.0002) - 0.02) == "-0.0198"
assert str(0.01 // Decimal(0.0004)) == "0"
assert str(Decimal(0.0004) // 0.01) == "0"

View File

@@ -1,36 +0,0 @@
import asyncio
import concurrent.futures
from bfxapi import Client
bfx = Client(logLevel='DEBUG')
async def run():
start = 1617519600000
candles = await bfx.rest.get_public_candles(start=start, symbol='tBTCUSD', end=None, tf='1h', sort=1, limit=1)
candle = candles[0]
price = candle[1]
assert price == 57394.61698309
orders_ids = []
trades = await bfx.rest.get_public_trades(start=1617519600000, limit=5, symbol='tBTCUSD', end=None, sort=1)
print(trades)
for trade in trades:
orders_ids.append(trade[0])
assert orders_ids == [657815316, 657815314, 657815312, 657815308, 657815304]
# check that strictly decreasing order id condition is always respected
# check that not increasing timestamp condition is always respected
orders_ids = []
timestamps = []
trades = await bfx.rest.get_public_trades(start=1617519600000, limit=5000, symbol='tLEOUSD', end=None, sort=1)
print(trades)
for trade in trades:
orders_ids.append(trade[0])
timestamps.append(trade[1])
assert not all(x > y for x, y in zip(orders_ids, orders_ids[1:])) is False
assert not all(x >= y for x, y in zip(orders_ids, orders_ids[1:])) is False
def test_get_public_trades():
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -0,0 +1,56 @@
import unittest
from typing import Optional
from dataclasses import dataclass
from ..types.labeler import _Type, generate_labeler_serializer, generate_recursive_serializer
class TestTypesLabeler(unittest.TestCase):
def test_generate_labeler_serializer(self):
@dataclass
class Test(_Type):
A: Optional[int]
B: float
C: str
labels = [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ]
serializer = generate_labeler_serializer("Test", Test, labels)
self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"),
msg="_Serializer should produce the right result.")
self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ],
msg="_Serializer::get_labels() should return the right list of labels.")
with self.assertRaises(AssertionError,
msg="_Serializer should raise an AssertionError if given " \
"fewer arguments than the serializer labels."):
serializer.parse(5, 65.0, "X")
def test_generate_recursive_serializer(self):
@dataclass
class Outer(_Type):
A: int
B: float
C: "Middle"
@dataclass
class Middle(_Type):
D: str
E: "Inner"
@dataclass
class Inner(_Type):
F: bool
inner = generate_labeler_serializer("Inner", Inner, ["F"])
middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], serializers={ "E": inner })
outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], serializers={ "C": middle })
self.assertEqual(outer.parse(10, 45.5, [ "Y", [ True ] ]), Outer(10, 45.5, Middle("Y", Inner(True))),
msg="_RecursiveSerializer should produce the right result.")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,29 @@
import unittest
from dataclasses import dataclass
from ..types.labeler import generate_labeler_serializer
from ..types.notification import _Type, _Notification, Notification
class TestTypesNotification(unittest.TestCase):
def test_types_notification(self):
@dataclass
class Test(_Type):
A: int
B: float
C: str
test = generate_labeler_serializer("Test", Test,
[ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ])
notification = _Notification[Test](test)
actual = notification.parse(*[ 1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], \
0, "SUCCESS", "This is just a test notification." ])
expected = Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"),
0, "SUCCESS", "This is just a test notification.")
self.assertEqual(actual, expected, msg="_Notification should produce the right notification.")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,17 @@
import unittest
from ..types import serializers
from ..types.labeler import _Type
class TestTypesSerializers(unittest.TestCase):
def test_types_serializers(self):
for serializer in map(serializers.__dict__.get, serializers.__serializers__):
self.assertTrue(issubclass(serializer.klass, _Type),
f"_Serializer <{serializer.name}>: .klass field must be a subclass " \
f"of _Type (got {serializer.klass}).")
self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__),
f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> " \
"must have matching labels and fields.")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,42 +0,0 @@
import pytest
import json
import time
import asyncio
from .helpers import (create_stubbed_client, ws_publish_connection_init, ws_publish_auth_accepted)
@pytest.mark.asyncio
async def test_ws_creates_new_socket():
client = create_stubbed_client()
client.ws.ws_capacity = 5
# publish connection created message
await ws_publish_connection_init(client.ws)
# create a bunch of websocket subscriptions
for symbol in ['tXRPBTC', 'tLTCUSD']:
await client.ws.subscribe('candles', symbol, timeframe='1m')
assert len(client.ws.sockets) == 1
assert client.ws.get_total_available_capcity() == 3
# subscribe to a few more to force the lib to create a new ws conenction
for symbol in ['tETHBTC', 'tBTCUSD', 'tETHUSD', 'tLTCBTC']:
await client.ws.subscribe('candles', symbol, timeframe='1m')
assert len(client.ws.sockets) == 2
assert client.ws.get_total_available_capcity() == 4
@pytest.mark.asyncio
async def test_ws_uses_authenticated_socket():
client = create_stubbed_client()
client.ws.ws_capacity = 2
# publish connection created message
await ws_publish_connection_init(client.ws)
# create a bunch of websocket subscriptions
for symbol in ['tXRPBTC', 'tLTCUSD', 'tETHBTC', 'tBTCUSD', 'tETHUSD', 'tLTCBTC']:
await client.ws.subscribe('candles', symbol, timeframe='1m')
# publish connection created message on socket (0 by default)
await ws_publish_connection_init(client.ws)
# send auth accepted (on socket by default)
await ws_publish_auth_accepted(client.ws)
# socket 0 should be the authenticated socket
assert client.ws.get_authenticated_socket().id == 0
# there should be no other authenticated sockets
for socket in client.ws.sockets.values():
if socket.id != 0:
assert socket.isAuthenticated == False

View File

@@ -1,65 +0,0 @@
import pytest
from .helpers import create_stubbed_client, ws_publish_connection_init, ws_publish_conf_accepted
@pytest.mark.asyncio
async def test_checksum_generation():
client = create_stubbed_client()
symbol = "tXRPBTC"
# publish connection created message
await ws_publish_connection_init(client.ws)
# publish checksum flag accepted
await ws_publish_conf_accepted(client.ws, 131072)
# subscribe to order book
await client.ws.subscribe('book', symbol)
## send subscription accepted
chanId = 123
await client.ws.publish({"event":"subscribed","channel":"book","chanId": chanId,"symbol": symbol,"prec":"P0","freq":"F0","len":"25","pair": symbol})
## send orderbook snapshot
await client.ws.publish("""[123, [[0.0000886,1,1060.55466114],[0.00008859,1,1000],[0.00008858,1,2713.47159343],[0.00008857,1,4276.92870916],[0.00008856,2,6764.75562319],
[0.00008854,1,5641.48532401],[0.00008853,1,2255.92632223],[0.0000885,1,2256.69584601],[0.00008848,2,3630.3],[0.00008845,1,28195.70625766],
[0.00008844,1,15571.7],[0.00008843,1,2500],[0.00008841,1,64196.16117814],[0.00008838,1,7500],[0.00008837,2,2764.12999012],[0.00008834,2,10886.476298],
[0.00008831,1,20000],[0.0000883,1,1000],[0.00008829,2,2517.22175358],[0.00008828,1,450.45],[0.00008827,1,13000],[0.00008824,1,1500],[0.0000882,1,300],
[0.00008817,1,3000],[0.00008816,1,100],[0.00008864,1,-481.8549041],[0.0000887,2,-2141.77009092],[0.00008871,1,-2256.45433182],[0.00008872,1,-2707.58122743],
[0.00008874,1,-5640.31794092],[0.00008876,1,-29004.93294912],[0.00008878,1,-2500],[0.0000888,1,-20000],[0.00008881,2,-2880.15595827],[0.00008882,1,-27705.42933984],
[0.00008883,1,-4509.83708214],[0.00008884,1,-1500],[0.00008885,1,-2500],[0.00008888,1,-902.91405442],[0.00008889,1,-900],[0.00008891,1,-7500],
[0.00008894,1,-775.08564697],[0.00008896,1,-150],[0.00008899,3,-11628.02590049],[0.000089,2,-1299.7],[0.00008902,2,-4841.8],[0.00008904,3,-25320.46250083],
[0.00008909,1,-14000],[0.00008913,1,-123947.999],[0.00008915,2,-28019.6]]]""", is_json=False)
## send some more price updates
await client.ws.publish("[{},[0.00008915,0,-1]]".format(chanId), is_json=False)
await client.ws.publish("[{},[0.00008837,1,56.54876269]]".format(chanId), is_json=False)
await client.ws.publish("[{},[0.00008873,1,-15699.9]]".format(chanId), is_json=False)
## check checksum is the same as expected
expected_checksum = 30026640
actual_checksum = client.ws.orderBooks[symbol].checksum()
assert expected_checksum == actual_checksum
@pytest.mark.asyncio
async def test_checksum_really_samll_numbers_generation():
client = create_stubbed_client()
symbol = "tVETBTC"
# publish connection created message
await ws_publish_connection_init(client.ws)
# publish checksum flag accepted
await ws_publish_conf_accepted(client.ws, 131072)
# subscribe to order book
await client.ws.subscribe('book', symbol)
## send subscription accepted
chanId = 123
await client.ws.publish({"event":"subscribed","channel":"book","chanId": chanId,"symbol": symbol,"prec":"P0","freq":"F0","len":"25","pair": symbol})
## send orderbook snapshot
await client.ws.publish("""[123, [[0.00000121,5,249013.0209708],[0.0000012,6,518315.33310128],[0.00000119,4,566200.89],[0.00000118,2,260000],[0.00000117,1,100000],
[0.00000116,2,160000],[0.00000114,1,60000],[0.00000113,2,198500],[0.00000112,1,60000],[0.0000011,1,60000],[0.00000106,2,113868.87735849],[0.00000105,2,105000],
[0.00000103,1,3000],[0.00000102,2,105000],[0.00000101,2,202970],[0.000001,2,21000],[7e-7,1,10000],[6.6e-7,1,10000],[6e-7,1,100000],[4.9e-7,1,10000],[2.5e-7,1,2000],
[6e-8,1,100000],[5e-8,1,200000],[1e-8,4,640000],[0.00000122,7,-312043.19],[0.00000123,6,-415094.8939744],[0.00000124,5,-348181.23],[0.00000125,1,-12000],
[0.00000126,2,-143872.31],[0.00000127,1,-5000],[0.0000013,1,-5000],[0.00000134,1,-8249.18938656],[0.00000135,2,-230043.1337899],[0.00000136,1,-13161.25184766],
[0.00000145,1,-2914],[0.0000015,3,-54448.5],[0.00000152,2,-5538.54849594],[0.00000153,1,-62691.75475079],[0.00000159,1,-2914],[0.0000016,1,-52631.10296831],
[0.00000164,1,-4000],[0.00000166,1,-3831.46784605],[0.00000171,1,-14575.17730379],[0.00000174,1,-3124.81815395],[0.0000018,1,-18000],[0.00000182,1,-16000],
[0.00000186,1,-4000],[0.00000189,1,-10000.686624],[0.00000191,1,-14500]]]""", is_json=False)
## send some more price updates
await client.ws.publish("[{},[0.00000121,4,228442.6609708]]".format(chanId), is_json=False)
await client.ws.publish("[{},[0.00000121,6,304023.8109708]]".format(chanId), is_json=False)
# await client.ws.publish("[{},[0.00008873,1,-15699.9]]".format(chanId), is_json=False)
## check checksum is the same as expected
expected_checksum = 1770440002
actual_checksum = client.ws.orderBooks[symbol].checksum()
assert expected_checksum == actual_checksum

View File

@@ -1,209 +0,0 @@
import pytest
import json
import asyncio
from .helpers import (create_stubbed_client, ws_publish_auth_accepted, ws_publish_connection_init,
EventWatcher)
@pytest.mark.asyncio
async def test_submit_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
## send new order
await client.ws.submit_order('tBTCUSD', 19000, 0.01, 'EXCHANGE MARKET')
last_sent = client.ws.get_last_sent_item()
sent_order_array = json.loads(last_sent['data'])
assert sent_order_array[1] == "on"
sent_order_json = sent_order_array[3]
assert sent_order_json['type'] == "EXCHANGE MARKET"
assert sent_order_json['symbol'] == "tBTCUSD"
assert sent_order_json['amount'] == "0.01"
assert sent_order_json['price'] == "19000"
@pytest.mark.asyncio
async def test_submit_update_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
## send new order
await client.ws.update_order(123, price=100, amount=0.01, hidden=True)
last_sent = client.ws.get_last_sent_item()
sent_order_array = json.loads(last_sent['data'])
assert sent_order_array[1] == "ou"
sent_order_json = sent_order_array[3]
# {"id": 123, "price": "100", "amount": "0.01", "flags": 64}
assert sent_order_json['id'] == 123
assert sent_order_json['price'] == "100"
assert sent_order_json['amount'] == "0.01"
assert sent_order_json['flags'] == 64
@pytest.mark.asyncio
async def test_submit_cancel_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
## send new order
await client.ws.cancel_order(123)
last_sent = client.ws.get_last_sent_item()
sent_order_array = json.loads(last_sent['data'])
assert sent_order_array[1] == "oc"
sent_order_json = sent_order_array[3]
assert sent_order_json['id'] == 123
@pytest.mark.asyncio
async def test_events_on_new_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
## look for new order confirmation
o_new = EventWatcher.watch(client.ws, 'order_new')
await client.ws.publish([0,"on",[1151718504,None,1548262833910,"tBTCUSD",1548262833379,1548262833410,-1,-1,"EXCHANGE LIMIT",None,None,None,0,"ACTIVE",None,None,15980,0,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
new_res = o_new.wait_until_complete()
assert new_res.amount_orig == -1
assert new_res.amount_filled == 0
assert new_res.price == 15980
assert new_res.type == 'EXCHANGE LIMIT'
## look for order update confirmation
o_update = EventWatcher.watch(client.ws, 'order_update')
await client.ws.publish([0,"ou",[1151718504,None,1548262833910,"tBTCUSD",1548262833379,1548262846964,-0.5,-1,"EXCHANGE LIMIT",None,None,None,0,"PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
update_res = o_update.wait_until_complete()
assert update_res.amount_orig == -1
assert float(update_res.amount_filled) == -0.5
assert update_res.price == 15980
assert update_res.type == 'EXCHANGE LIMIT'
## look for closed notification
o_closed = EventWatcher.watch(client.ws, 'order_closed')
await client.ws.publish([0,"oc",[1151718504,None,1548262833910,"tBTCUSD",1548262833379,1548262888016,0,-1,"EXCHANGE LIMIT",None,None,None,0,"EXECUTED @ 15980.0(-0.5): was PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
closed_res = o_closed.wait_until_complete()
assert new_res.amount_orig == -1
assert new_res.amount_filled == 0
assert new_res.price == 15980
assert new_res.type == 'EXCHANGE LIMIT'
@pytest.mark.asyncio
async def test_events_on_cancel_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
## Create new order
await client.ws.publish([0,"on",[1151718565,None,1548325124885,"tBTCUSD",1548325123435,1548325123460,1,1,"EXCHANGE LIMIT",None,None,None,0,"ACTIVE",None,None,10,0,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
## look for order closed confirmation
o_close = EventWatcher.watch(client.ws, 'order_closed')
await client.ws.publish([0,"oc",[1151718565,None,1548325124885,"tBTCUSD",1548325123435,1548325123548,1,1,"EXCHANGE LIMIT",None,None,None,0,"CANCELED",None,None,10,0,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
close_res = o_close.wait_until_complete()
assert close_res.amount_orig == 1
assert float(close_res.amount_filled) == 0
assert close_res.price == 10
assert close_res.type == 'EXCHANGE LIMIT'
@pytest.mark.asyncio
async def test_closed_callback_on_submit_order_closed():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
async def c(order):
client.ws._emit('c1', order)
callback_wait = EventWatcher.watch(client.ws, 'c1')
# override cid generation
client.ws.orderManager._gen_unique_cid = lambda: 123
await client.ws.submit_order('tBTCUSD', 19000, 0.01, 'EXCHANGE MARKET', onClose=c)
await client.ws.publish([0,"oc",[123,None,1548262833910,"tBTCUSD",1548262833379,1548262888016,0,-1,"EXCHANGE LIMIT",None,None,None,0,"EXECUTED @ 15980.0(-0.5): was PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
callback_wait.wait_until_complete()
@pytest.mark.asyncio
async def test_confirmed_callback_on_submit_order_closed():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
async def c(order):
client.ws._emit('c1', order)
callback_wait = EventWatcher.watch(client.ws, 'c1')
# override cid generation
client.ws.orderManager._gen_unique_cid = lambda: 123
await client.ws.submit_order('tBTCUSD', 19000, 0.01, 'EXCHANGE MARKET', onConfirm=c)
await client.ws.publish([0,"oc",[123,None,1548262833910,"tBTCUSD",1548262833379,1548262888016,0,-1,"EXCHANGE LIMIT",None,None,None,0,"EXECUTED @ 15980.0(-0.5): was PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
callback_wait.wait_until_complete()
@pytest.mark.asyncio
async def test_confirmed_callback_on_submit_new_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
async def c(order):
client.ws._emit('c1', order)
callback_wait = EventWatcher.watch(client.ws, 'c1')
# override cid generation
client.ws.orderManager._gen_unique_cid = lambda: 123
await client.ws.submit_order('tBTCUSD', 19000, 0.01, 'EXCHANGE MARKET', onConfirm=c)
await client.ws.publish([0,"on",[123,None,1548262833910,"tBTCUSD",1548262833379,1548262833410,-1,-1,"EXCHANGE LIMIT",None,None,None,0,"ACTIVE",None,None,15980,0,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
callback_wait.wait_until_complete()
@pytest.mark.asyncio
async def test_confirmed_callback_on_submit_order_update():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
async def c(order):
client.ws._emit('c1', order)
callback_wait = EventWatcher.watch(client.ws, 'c1')
# override cid generation
client.ws.orderManager._gen_unique_cid = lambda: 123
await client.ws.update_order(123, price=100, onConfirm=c)
await client.ws.publish([0,"ou",[123,None,1548262833910,"tBTCUSD",1548262833379,1548262846964,-0.5,-1,"EXCHANGE LIMIT",None,None,None,0,"PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
callback_wait.wait_until_complete()
@pytest.mark.asyncio
async def test_confirmed_callback_on_submit_cancel_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
async def c(order):
client.ws._emit('c1', order)
callback_wait = EventWatcher.watch(client.ws, 'c1')
# override cid generation
client.ws.orderManager._gen_unique_cid = lambda: 123
await client.ws.cancel_order(123, onConfirm=c)
await client.ws.publish([0,"oc",[123,None,1548262833910,"tBTCUSD",1548262833379,1548262888016,0,-1,"EXCHANGE LIMIT",None,None,None,0,"EXECUTED @ 15980.0(-0.5): was PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
callback_wait.wait_until_complete()
@pytest.mark.asyncio
async def test_confirmed_callback_on_submit_cancel_group_order():
client = create_stubbed_client()
# publish connection created message
await ws_publish_connection_init(client.ws)
## send auth accepted
await ws_publish_auth_accepted(client.ws)
async def c(order):
client.ws._emit('c1', order)
callback_wait = EventWatcher.watch(client.ws, 'c1')
# override cid generation
client.ws.orderManager._gen_unique_cid = lambda: 123
await client.ws.cancel_order_group(123, onConfirm=c)
await client.ws.publish([0,"oc",[1548262833910,123,1548262833910,"tBTCUSD",1548262833379,1548262888016,0,-1,"EXCHANGE LIMIT",None,None,None,0,"EXECUTED @ 15980.0(-0.5): was PARTIALLY FILLED @ 15980.0(-0.5)",None,None,15980,15980,0,0,None,None,None,0,0,None,None,None,"API>BFX",None,None,None]])
callback_wait.wait_until_complete()

View File

@@ -1,140 +0,0 @@
import pytest
import json
from .helpers import (create_stubbed_client, ws_publish_connection_init, EventWatcher)
@pytest.mark.asyncio
async def test_submit_subscribe():
client = create_stubbed_client()
symb = 'tXRPBTC'
# publish connection created message
await ws_publish_connection_init(client.ws)
# Create new subscription to orderbook
await client.ws.subscribe('book', symb)
last_sent = client.ws.get_last_sent_item()
sent_sub = json.loads(last_sent['data'])
# {'time': 1548327054030, 'data': '{"event": "subscribe", "channel": "book", "symbol": "tXRPBTC"}'}
assert sent_sub['event'] == "subscribe"
assert sent_sub['channel'] == "book"
assert sent_sub['symbol'] == symb
# create new subscription to trades
await client.ws.subscribe('trades', symb)
last_sent = client.ws.get_last_sent_item()
sent_sub = json.loads(last_sent['data'])
# {'event': 'subscribe', 'channel': 'trades', 'symbol': 'tBTCUSD'}
assert sent_sub['event'] == 'subscribe'
assert sent_sub['channel'] == 'trades'
assert sent_sub['symbol'] == symb
# create new subscription to candles
await client.ws.subscribe('candles', symb, timeframe='1m')
last_sent = client.ws.get_last_sent_item()
sent_sub = json.loads(last_sent['data'])
#{'event': 'subscribe', 'channel': 'candles', 'symbol': 'tBTCUSD', 'key': 'trade:1m:tBTCUSD'}
assert sent_sub['event'] == 'subscribe'
assert sent_sub['channel'] == 'candles'
assert sent_sub['key'] == 'trade:1m:{}'.format(symb)
@pytest.mark.asyncio
async def test_event_subscribe():
client = create_stubbed_client()
symb = 'tXRPBTC'
pair = 'XRPBTC'
# publish connection created message
await ws_publish_connection_init(client.ws)
# create a new subscription
await client.ws.subscribe('trades', symb)
# announce subscription was successful
sub_watch = EventWatcher.watch(client.ws, 'subscribed')
await client.ws.publish({"event":"subscribed","channel":"trades","chanId":2,"symbol":symb,"pair":pair})
s_res = sub_watch.wait_until_complete()
assert s_res.channel_name == 'trades'
assert s_res.symbol == symb
assert s_res.is_subscribed_bool == True
assert s_res.chan_id == 2
@pytest.mark.asyncio
async def test_submit_unsubscribe():
client = create_stubbed_client()
symb = 'tXRPBTC'
pair = 'XRPBTC'
# publish connection created message
await ws_publish_connection_init(client.ws)
# create new subscription to trades
await client.ws.subscribe('trades', symb)
# announce subscription was successful
sub_watch = EventWatcher.watch(client.ws, 'subscribed')
await client.ws.publish({"event":"subscribed","channel":"trades","chanId":2,"symbol":symb,"pair":pair})
s_res = sub_watch.wait_until_complete()
# unsubscribe from channel
await s_res.unsubscribe()
last_sent = client.ws.get_last_sent_item()
sent_unsub = json.loads(last_sent['data'])
# {'event': 'unsubscribe', 'chanId': 2}
assert sent_unsub['event'] == 'unsubscribe'
assert sent_unsub['chanId'] == 2
@pytest.mark.asyncio
async def test_event_unsubscribe():
client = create_stubbed_client()
symb = 'tXRPBTC'
pair = 'XRPBTC'
# publish connection created message
await ws_publish_connection_init(client.ws)
# create new subscription to trades
await client.ws.subscribe('trades', symb)
# announce subscription was successful
sub_watch = EventWatcher.watch(client.ws, 'subscribed')
await client.ws.publish({"event":"subscribed","channel":"trades","chanId":2,"symbol":symb,"pair":pair})
s_res = sub_watch.wait_until_complete()
# unsubscribe from channel
await s_res.unsubscribe()
last_sent = client.ws.get_last_sent_item()
sent_unsub = json.loads(last_sent['data'])
# publish confirmation of unsubscribe
unsub_watch = EventWatcher.watch(client.ws, 'unsubscribed')
await client.ws.publish({"event":"unsubscribed","status":"OK","chanId":2})
unsub_res = unsub_watch.wait_until_complete()
assert s_res.channel_name == 'trades'
assert s_res.symbol == symb
assert s_res.is_subscribed_bool == False
assert s_res.chan_id == 2
@pytest.mark.asyncio
async def test_submit_resubscribe():
client = create_stubbed_client()
symb = 'tXRPBTC'
pair = 'XRPBTC'
# publish connection created message
await ws_publish_connection_init(client.ws)
# request two new subscriptions
await client.ws.subscribe('book', symb)
await client.ws.subscribe('trades', symb)
# confirm subscriptions
await client.ws.publish({"event":"subscribed","channel":"trades","chanId":2,"symbol":symb,"pair":pair})
await client.ws.publish({"event":"subscribed","channel":"book","chanId":3,"symbol":symb,"prec":"P0","freq":"F0","len":"25","pair":pair})
# call resubscribe all
await client.ws.resubscribe_all()
## assert that 2 unsubscribe requests were sent
last_sent = client.ws.get_sent_items()[-2:]
for i in last_sent:
data = json.loads(i['data'])
assert data['event'] == 'unsubscribe'
assert (data['chanId'] == 2 or data['chanId'] == 3)
## confirm unsubscriptions
await client.ws.publish({"event":"unsubscribed","status":"OK","chanId":2})
await client.ws.publish({"event":"unsubscribed","status":"OK","chanId":3})
## confirm subscriptions
# await client.ws.publish({"event":"subscribed","channel":"trades","chanId":2,"symbol":symb,"pair":pair})
# await client.ws.publish({"event":"subscribed","channel":"book","chanId":3,"symbol":symb,"prec":"P0","freq":"F0","len":"25","pair":pair})
# wait for emit of event
n_last_sent = client.ws.get_sent_items()[-2:]
for i in n_last_sent:
data = json.loads(i['data'])
# print (data)
assert data['event'] == 'subscribe'
assert (data['channel'] == 'book' or data['channel'] == 'trades')
assert data['symbol'] == symb

26
bfxapi/types/__init__.py Normal file
View File

@@ -0,0 +1,26 @@
from .dataclasses import JSON, \
PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \
TickersHistory, TradingPairTrade, FundingCurrencyTrade, \
TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \
FundingCurrencyRawBook, Statistic, Candle, \
DerivativesStatus, Liquidation, Leaderboard, \
FundingStatistic, PulseProfile, PulseMessage, \
TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate
from .dataclasses import \
UserInfo, LoginHistory, BalanceAvailable, \
Order, Position, Trade, \
FundingTrade, OrderTrade, Ledger, \
FundingOffer, FundingCredit, FundingLoan, \
FundingAutoRenew, FundingInfo, Wallet, \
Transfer, Withdrawal, DepositAddress, \
LightningNetworkInvoice, Movement, SymbolMarginInfo, \
BaseMarginInfo, PositionClaim, PositionIncreaseInfo, \
PositionIncrease, PositionHistory, PositionSnapshot, \
PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits
from .dataclasses import \
InvoiceSubmission, InvoicePage, InvoiceStats, \
CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit
from .notification import Notification

694
bfxapi/types/dataclasses.py Normal file
View File

@@ -0,0 +1,694 @@
from typing import Union, Type, \
List, Dict, Literal, Optional, Any
from dataclasses import dataclass
from .labeler import _Type, partial, compose
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
#region Dataclass definitions for types of public use
@dataclass
class PlatformStatus(_Type):
status: int
@dataclass
class TradingPairTicker(_Type):
bid: float
bid_size: float
ask: float
ask_size: float
daily_change: float
daily_change_relative: float
last_price: float
volume: float
high: float
low: float
@dataclass
class FundingCurrencyTicker(_Type):
frr: float
bid: float
bid_period: int
bid_size: float
ask: float
ask_period: int
ask_size: float
daily_change: float
daily_change_relative: float
last_price: float
volume: float
high: float
low: float
frr_amount_available: float
@dataclass
class TickersHistory(_Type):
symbol: str
bid: float
ask: float
mts: int
@dataclass
class TradingPairTrade(_Type):
id: int
mts: int
amount: float
price: float
@dataclass
class FundingCurrencyTrade(_Type):
id: int
mts: int
amount: float
rate: float
period: int
@dataclass
class TradingPairBook(_Type):
price: float
count: int
amount: float
@dataclass
class FundingCurrencyBook(_Type):
rate: float
period: int
count: int
amount: float
@dataclass
class TradingPairRawBook(_Type):
order_id: int
price: float
amount: float
@dataclass
class FundingCurrencyRawBook(_Type):
offer_id: int
period: int
rate: float
amount: float
@dataclass
class Statistic(_Type):
mts: int
value: float
@dataclass
class Candle(_Type):
mts: int
open: int
close: int
high: int
low: int
volume: float
@dataclass
class DerivativesStatus(_Type):
mts: int
deriv_price: float
spot_price: float
insurance_fund_balance: float
next_funding_evt_mts: int
next_funding_accrued: float
next_funding_step: int
current_funding: float
mark_price: float
open_interest: float
clamp_min: float
clamp_max: float
@dataclass
class Liquidation(_Type):
pos_id: int
mts: int
symbol: str
amount: float
base_price: float
is_match: int
is_market_sold: int
price_acquired: float
@dataclass
class Leaderboard(_Type):
mts: int
username: str
ranking: int
value: float
twitter_handle: Optional[str]
@dataclass
class FundingStatistic(_Type):
mts: int
frr: float
avg_period: float
funding_amount: float
funding_amount_used: float
funding_below_threshold: float
@dataclass
class PulseProfile(_Type):
puid: str
mts: int
nickname: str
picture: str
text: str
twitter_handle: str
followers: int
following: int
tipping_status: int
@dataclass
class PulseMessage(_Type):
pid: str
mts: int
puid: str
title: str
content: str
is_pin: int
is_public: int
comments_disabled: int
tags: List[str]
attachments: List[str]
meta: List[JSON]
likes: int
profile: PulseProfile
comments: int
@dataclass
class TradingMarketAveragePrice(_Type):
price_avg: float
amount: float
@dataclass
class FundingMarketAveragePrice(_Type):
rate_avg: float
amount: float
@dataclass
class FxRate(_Type):
current_rate: float
#endregion
#region Dataclass definitions for types of auth use
@dataclass
class UserInfo(_Type):
id: int
email: str
username: str
mts_account_create: int
verified: int
verification_level: int
timezone: str
locale: str
company: str
email_verified: int
mts_master_account_create: int
group_id: int
master_account_id: int
inherit_master_account_verification: int
is_group_master: int
group_withdraw_enabled: int
ppt_enabled: int
merchant_enabled: int
competition_enabled: int
two_factors_authentication_modes: List[str]
is_securities_master: int
securities_enabled: int
allow_disable_ctxswitch: int
time_last_login: int
ctxtswitch_disabled: int
comp_countries: List[str]
compl_countries_resid: List[str]
is_merchant_enterprise: int
@dataclass
class LoginHistory(_Type):
id: int
time: int
ip: str
extra_info: JSON
@dataclass
class BalanceAvailable(_Type):
amount: float
@dataclass
class Order(_Type):
id: int
gid: int
cid: int
symbol: str
mts_create: int
mts_update: int
amount: float
amount_orig: float
order_type: str
type_prev: str
mts_tif: int
flags: int
order_status: str
price: float
price_avg: float
price_trailing: float
price_aux_limit: float
notify: int
hidden: int
placed_id: int
routing: str
meta: JSON
@dataclass
class Position(_Type):
symbol: str
status: str
amount: float
base_price: float
margin_funding: float
margin_funding_type: int
pl: float
pl_perc: float
price_liq: float
leverage: float
position_id: int
mts_create: int
mts_update: int
type: int
collateral: float
collateral_min: float
meta: JSON
@dataclass
class Trade(_Type):
id: int
symbol: str
mts_create: int
order_id: int
exec_amount: float
exec_price: float
order_type: str
order_price: float
maker:int
fee: float
fee_currency: str
cid: int
@dataclass()
class FundingTrade(_Type):
id: int
currency: str
mts_create: int
offer_id: int
amount: float
rate: float
period: int
@dataclass
class OrderTrade(_Type):
id: int
symbol: str
mts_create: int
order_id: int
exec_amount: float
exec_price: float
maker:int
fee: float
fee_currency: str
cid: int
@dataclass
class Ledger(_Type):
id: int
currency: str
mts: int
amount: float
balance: float
description: str
@dataclass
class FundingOffer(_Type):
id: int
symbol: str
mts_create: int
mts_update: int
amount: float
amount_orig: float
offer_type: str
flags: int
offer_status: str
rate: float
period: int
notify: int
hidden: int
renew: int
@dataclass
class FundingCredit(_Type):
id: int
symbol: str
side: int
mts_create: int
mts_update: int
amount: float
flags: int
status: str
rate_type: str
rate: float
period: int
mts_opening: int
mts_last_payout: int
notify: int
hidden: int
renew: int
no_close: int
position_pair: str
@dataclass
class FundingLoan(_Type):
id: int
symbol: str
side: int
mts_create: int
mts_update: int
amount: float
flags: int
status: str
rate_type: str
rate: float
period: int
mts_opening: int
mts_last_payout: int
notify: int
hidden: int
renew: int
no_close: int
@dataclass
class FundingAutoRenew(_Type):
currency: str
period: int
rate: float
threshold: float
@dataclass()
class FundingInfo(_Type):
yield_loan: float
yield_lend: float
duration_loan: float
duration_lend: float
@dataclass
class Wallet(_Type):
wallet_type: str
currency: str
balance: float
unsettled_interest: float
available_balance: float
last_change: str
trade_details: JSON
@dataclass
class Transfer(_Type):
mts: int
wallet_from: str
wallet_to: str
currency: str
currency_to: str
amount: int
@dataclass
class Withdrawal(_Type):
withdrawal_id: int
method: str
payment_id: str
wallet: str
amount: float
withdrawal_fee: float
@dataclass
class DepositAddress(_Type):
method: str
currency_code: str
address: str
pool_address: str
@dataclass
class LightningNetworkInvoice(_Type):
invoice_hash: str
invoice: str
amount: str
@dataclass
class Movement(_Type):
id: str
currency: str
currency_name: str
mts_start: int
mts_update: int
status: str
amount: int
fees: int
destination_address: str
transaction_id: str
withdraw_transaction_note: str
@dataclass
class SymbolMarginInfo(_Type):
symbol: str
tradable_balance: float
gross_balance: float
buy: float
sell: float
@dataclass
class BaseMarginInfo(_Type):
user_pl: float
user_swaps: float
margin_balance: float
margin_net: float
margin_min: float
@dataclass
class PositionClaim(_Type):
symbol: str
position_status: str
amount: float
base_price: float
margin_funding: float
margin_funding_type: int
position_id: int
mts_create: int
mts_update: int
pos_type: int
collateral: str
min_collateral: str
meta: JSON
@dataclass
class PositionIncreaseInfo(_Type):
max_pos: int
current_pos: float
base_currency_balance: float
tradable_balance_quote_currency: float
tradable_balance_quote_total: float
tradable_balance_base_currency: float
tradable_balance_base_total: float
funding_avail: float
funding_value: float
funding_required: float
funding_value_currency: str
funding_required_currency: str
@dataclass
class PositionIncrease(_Type):
symbol: str
amount: float
base_price: float
@dataclass
class PositionHistory(_Type):
symbol: str
status: str
amount: float
base_price: float
funding: float
funding_type: int
position_id: int
mts_create: int
mts_update: int
@dataclass
class PositionSnapshot(_Type):
symbol: str
status: str
amount: float
base_price: float
funding: float
funding_type: int
position_id: int
mts_create: int
mts_update: int
@dataclass
class PositionAudit(_Type):
symbol: str
status: str
amount: float
base_price: float
funding: float
funding_type: int
position_id: int
mts_create: int
mts_update: int
type: int
collateral: float
collateral_min: float
meta: JSON
@dataclass
class DerivativePositionCollateral(_Type):
status: int
@dataclass
class DerivativePositionCollateralLimits(_Type):
min_collateral: float
max_collateral: float
#endregion
#region Dataclass definitions for types of merchant use
@compose(dataclass, partial)
class InvoiceSubmission(_Type):
id: str
t: int
type: Literal["ECOMMERCE", "POS"]
duration: int
amount: float
currency: str
order_id: str
pay_currencies: List[str]
webhook: str
redirect_url: str
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]
customer_info: "CustomerInfo"
invoices: List["Invoice"]
payment: "Payment"
additional_payments: List["Payment"]
merchant_name: str
@classmethod
def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission":
if "customer_info" in data and data["customer_info"] is not None:
data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"])
for index, invoice in enumerate(data["invoices"]):
data["invoices"][index] = InvoiceSubmission.Invoice(**invoice)
if "payment" in data and data["payment"] is not None:
data["payment"] = InvoiceSubmission.Payment(**data["payment"])
if "additional_payments" in data and data["additional_payments"] is not None:
for index, additional_payment in enumerate(data["additional_payments"]):
data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment)
return InvoiceSubmission(**data)
@compose(dataclass, partial)
class CustomerInfo:
nationality: str
resid_country: str
resid_state: str
resid_city: str
resid_zip_code: str
resid_street: str
resid_building_no: str
full_name: str
email: str
tos_accepted: bool
@compose(dataclass, partial)
class Invoice:
amount: float
currency: str
pay_currency: str
pool_currency: str
address: str
ext: JSON
@compose(dataclass, partial)
class Payment:
txid: str
amount: float
currency: str
method: str
status: Literal["CREATED", "COMPLETED", "PROCESSING"]
confirmations: int
created_at: str
updated_at: str
deposit_id: int
ledger_id: int
force_completed: bool
amount_diff: str
@dataclass
class InvoicePage(_Type):
page: int
page_size: int
sort: Literal["asc", "desc"]
sort_field: Literal["t", "amount", "status"]
total_pages: int
total_items: int
items: List[InvoiceSubmission]
@classmethod
def parse(cls, data: Dict[str, Any]) -> "InvoicePage":
for index, item in enumerate(data["items"]):
data["items"][index] = InvoiceSubmission.parse(item)
return InvoicePage(**data)
@dataclass
class InvoiceStats(_Type):
time: str
count: float
@dataclass
class CurrencyConversion(_Type):
base_currency: str
convert_currency: str
created: int
@dataclass
class MerchantDeposit(_Type):
id: int
invoice_id: Optional[str]
order_id: Optional[str]
type: Literal["ledger", "deposit"]
amount: float
t: int
txid: str
currency: str
method: str
pay_method: str
@dataclass
class MerchantUnlinkedDeposit(_Type):
id: int
method: str
currency: str
created_at: int
updated_at: int
amount: float
fee: float
txid: str
address: str
payment_id: Optional[int]
status: str
note: Optional[str]
#endregion

96
bfxapi/types/labeler.py Normal file
View File

@@ -0,0 +1,96 @@
from typing import Type, Generic, TypeVar, Iterable, \
Dict, List, Tuple, Any, cast
T = TypeVar("T", bound="_Type")
def compose(*decorators):
def wrapper(function):
for decorator in reversed(decorators):
function = decorator(function)
return function
return wrapper
def partial(cls):
def __init__(self, **kwargs):
for annotation in self.__annotations__.keys():
if annotation not in kwargs:
self.__setattr__(annotation, None)
else: self.__setattr__(annotation, kwargs[annotation])
kwargs.pop(annotation, None)
if len(kwargs) != 0:
raise TypeError(f"{cls.__name__}.__init__() got an unexpected keyword argument '{list(kwargs.keys())[0]}'")
cls.__init__ = __init__
return cls
class _Type:
"""
Base class for any dataclass serializable by the _Serializer generic class.
"""
class _Serializer(Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str],
*, flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ]):
self.name, self.klass, self.__labels, self.__flat, self.__ignore = name, klass, labels, flat, ignore
def _serialize(self, *args: Any) -> Iterable[Tuple[str, Any]]:
if self.__flat:
args = tuple(_Serializer.__flatten(list(args)))
if len(self.__labels) > len(args):
raise AssertionError(f"{self.name} -> <labels> and <*args> " \
"arguments should contain the same amount of elements.")
for index, label in enumerate(self.__labels):
if label not in self.__ignore:
yield label, args[index]
def parse(self, *values: Any) -> T:
return cast(T, self.klass(**dict(self._serialize(*values))))
def get_labels(self) -> List[str]:
return [ label for label in self.__labels if label not in self.__ignore ]
@classmethod
def __flatten(cls, array: List[Any]) -> List[Any]:
if len(array) == 0:
return array
if isinstance(array[0], list):
return cls.__flatten(array[0]) + cls.__flatten(array[1:])
return array[:1] + cls.__flatten(array[1:])
class _RecursiveSerializer(_Serializer, Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str],
*, serializers: Dict[str, _Serializer[Any]],
flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ]):
super().__init__(name, klass, labels, flat=flat, ignore=ignore)
self.serializers = serializers
def parse(self, *values: Any) -> T:
serialization = dict(self._serialize(*values))
for key in serialization:
if key in self.serializers.keys():
serialization[key] = self.serializers[key].parse(*serialization[key])
return cast(T, self.klass(**serialization))
def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str],
*, flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ]
) -> _Serializer[T]:
return _Serializer[T](name, klass, labels, \
flat=flat, ignore=ignore)
def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str],
*, serializers: Dict[str, _Serializer[Any]],
flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ]
) -> _RecursiveSerializer[T]:
return _RecursiveSerializer[T](name, klass, labels, \
serializers=serializers, flat=flat, ignore=ignore)

View File

@@ -0,0 +1,38 @@
from typing import List, Optional, Any, Generic, TypeVar, cast
from dataclasses import dataclass
from .labeler import _Type, _Serializer
T = TypeVar("T")
@dataclass
class Notification(_Type, Generic[T]):
mts: int
type: str
message_id: Optional[int]
data: T
code: Optional[int]
status: str
text: str
class _Notification(_Serializer, Generic[T]):
__LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "data", "code", "status", "text" ]
def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False):
super().__init__("Notification", Notification, _Notification.__LABELS, ignore = [ "_PLACEHOLDER" ])
self.serializer, self.is_iterable = serializer, is_iterable
def parse(self, *values: Any) -> Notification[T]:
notification = cast(Notification[T], Notification(**dict(self._serialize(*values))))
if isinstance(self.serializer, _Serializer):
data = cast(List[Any], notification.data)
if not self.is_iterable:
if len(data) == 1 and isinstance(data[0], list):
data = data[0]
notification.data = self.serializer.parse(*data)
else: notification.data = cast(T, [ self.serializer.parse(*sub_data) for sub_data in data ])
return notification

959
bfxapi/types/serializers.py Normal file
View File

@@ -0,0 +1,959 @@
from .import dataclasses
from .labeler import \
generate_labeler_serializer, generate_recursive_serializer
#pylint: disable-next=unused-import
from .notification import _Notification
__serializers__ = [
"PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker",
"TickersHistory", "TradingPairTrade", "FundingCurrencyTrade",
"TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook",
"FundingCurrencyRawBook", "Statistic", "Candle",
"DerivativesStatus", "Liquidation", "Leaderboard",
"FundingStatistic", "PulseProfile", "PulseMessage",
"TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate",
"UserInfo", "LoginHistory", "BalanceAvailable",
"Order", "Position", "Trade",
"FundingTrade", "OrderTrade", "Ledger",
"FundingOffer", "FundingCredit", "FundingLoan",
"FundingAutoRenew", "FundingInfo", "Wallet",
"Transfer", "Withdrawal", "DepositAddress",
"LightningNetworkInvoice", "Movement", "SymbolMarginInfo",
"BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo",
"PositionIncrease", "PositionHistory", "PositionSnapshot",
"PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits",
]
#region Serializer definitions for types of public use
PlatformStatus = generate_labeler_serializer(
name="PlatformStatus",
klass=dataclasses.PlatformStatus,
labels=[
"status"
]
)
TradingPairTicker = generate_labeler_serializer(
name="TradingPairTicker",
klass=dataclasses.TradingPairTicker,
labels=[
"bid",
"bid_size",
"ask",
"ask_size",
"daily_change",
"daily_change_relative",
"last_price",
"volume",
"high",
"low"
]
)
FundingCurrencyTicker = generate_labeler_serializer(
name="FundingCurrencyTicker",
klass=dataclasses.FundingCurrencyTicker,
labels=[
"frr",
"bid",
"bid_period",
"bid_size",
"ask",
"ask_period",
"ask_size",
"daily_change",
"daily_change_relative",
"last_price",
"volume",
"high",
"low",
"_PLACEHOLDER",
"_PLACEHOLDER",
"frr_amount_available"
]
)
TickersHistory = generate_labeler_serializer(
name="TickersHistory",
klass=dataclasses.TickersHistory,
labels=[
"symbol",
"bid",
"_PLACEHOLDER",
"ask",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mts"
]
)
TradingPairTrade = generate_labeler_serializer(
name="TradingPairTrade",
klass=dataclasses.TradingPairTrade,
labels=[
"id",
"mts",
"amount",
"price"
]
)
FundingCurrencyTrade = generate_labeler_serializer(
name="FundingCurrencyTrade",
klass=dataclasses.FundingCurrencyTrade,
labels=[
"id",
"mts",
"amount",
"rate",
"period"
]
)
TradingPairBook = generate_labeler_serializer(
name="TradingPairBook",
klass=dataclasses.TradingPairBook,
labels=[
"price",
"count",
"amount"
]
)
FundingCurrencyBook = generate_labeler_serializer(
name="FundingCurrencyBook",
klass=dataclasses.FundingCurrencyBook,
labels=[
"rate",
"period",
"count",
"amount"
]
)
TradingPairRawBook = generate_labeler_serializer(
name="TradingPairRawBook",
klass=dataclasses.TradingPairRawBook,
labels=[
"order_id",
"price",
"amount"
]
)
FundingCurrencyRawBook = generate_labeler_serializer(
name="FundingCurrencyRawBook",
klass=dataclasses.FundingCurrencyRawBook,
labels=[
"offer_id",
"period",
"rate",
"amount"
]
)
Statistic = generate_labeler_serializer(
name="Statistic",
klass=dataclasses.Statistic,
labels=[
"mts",
"value"
]
)
Candle = generate_labeler_serializer(
name="Candle",
klass=dataclasses.Candle,
labels=[
"mts",
"open",
"close",
"high",
"low",
"volume"
]
)
DerivativesStatus = generate_labeler_serializer(
name="DerivativesStatus",
klass=dataclasses.DerivativesStatus,
labels=[
"mts",
"_PLACEHOLDER",
"deriv_price",
"spot_price",
"_PLACEHOLDER",
"insurance_fund_balance",
"_PLACEHOLDER",
"next_funding_evt_mts",
"next_funding_accrued",
"next_funding_step",
"_PLACEHOLDER",
"current_funding",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mark_price",
"_PLACEHOLDER",
"_PLACEHOLDER",
"open_interest",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"clamp_min",
"clamp_max"
]
)
Liquidation = generate_labeler_serializer(
name="Liquidation",
klass=dataclasses.Liquidation,
labels=[
"_PLACEHOLDER",
"pos_id",
"mts",
"_PLACEHOLDER",
"symbol",
"amount",
"base_price",
"_PLACEHOLDER",
"is_match",
"is_market_sold",
"_PLACEHOLDER",
"price_acquired"
]
)
Leaderboard = generate_labeler_serializer(
name="Leaderboard",
klass=dataclasses.Leaderboard,
labels=[
"mts",
"_PLACEHOLDER",
"username",
"ranking",
"_PLACEHOLDER",
"_PLACEHOLDER",
"value",
"_PLACEHOLDER",
"_PLACEHOLDER",
"twitter_handle"
]
)
FundingStatistic = generate_labeler_serializer(
name="FundingStatistic",
klass=dataclasses.FundingStatistic,
labels=[
"mts",
"_PLACEHOLDER",
"_PLACEHOLDER",
"frr",
"avg_period",
"_PLACEHOLDER",
"_PLACEHOLDER",
"funding_amount",
"funding_amount_used",
"_PLACEHOLDER",
"_PLACEHOLDER",
"funding_below_threshold"
]
)
PulseProfile = generate_labeler_serializer(
name="PulseProfile",
klass=dataclasses.PulseProfile,
labels=[
"puid",
"mts",
"_PLACEHOLDER",
"nickname",
"_PLACEHOLDER",
"picture",
"text",
"_PLACEHOLDER",
"_PLACEHOLDER",
"twitter_handle",
"_PLACEHOLDER",
"followers",
"following",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"tipping_status"
]
)
PulseMessage = generate_recursive_serializer(
name="PulseMessage",
klass=dataclasses.PulseMessage,
serializers={ "profile": PulseProfile },
labels=[
"pid",
"mts",
"_PLACEHOLDER",
"puid",
"_PLACEHOLDER",
"title",
"content",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_pin",
"is_public",
"comments_disabled",
"tags",
"attachments",
"meta",
"likes",
"_PLACEHOLDER",
"_PLACEHOLDER",
"profile",
"comments",
"_PLACEHOLDER",
"_PLACEHOLDER"
]
)
TradingMarketAveragePrice = generate_labeler_serializer(
name="TradingMarketAveragePrice",
klass=dataclasses.TradingMarketAveragePrice,
labels=[
"price_avg",
"amount"
]
)
FundingMarketAveragePrice = generate_labeler_serializer(
name="FundingMarketAveragePrice",
klass=dataclasses.FundingMarketAveragePrice,
labels=[
"rate_avg",
"amount"
]
)
FxRate = generate_labeler_serializer(
name="FxRate",
klass=dataclasses.FxRate,
labels=[
"current_rate"
]
)
#endregion
#region Serializer definitions for types of auth use
UserInfo = generate_labeler_serializer(
name="UserInfo",
klass=dataclasses.UserInfo,
labels=[
"id",
"email",
"username",
"mts_account_create",
"verified",
"verification_level",
"_PLACEHOLDER",
"timezone",
"locale",
"company",
"email_verified",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mts_master_account_create",
"group_id",
"master_account_id",
"inherit_master_account_verification",
"is_group_master",
"group_withdraw_enabled",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ppt_enabled",
"merchant_enabled",
"competition_enabled",
"two_factors_authentication_modes",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_securities_master",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"securities_enabled",
"allow_disable_ctxswitch",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"time_last_login",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ctxtswitch_disabled",
"_PLACEHOLDER",
"comp_countries",
"compl_countries_resid",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_merchant_enterprise"
]
)
LoginHistory = generate_labeler_serializer(
name="LoginHistory",
klass=dataclasses.LoginHistory,
labels=[
"id",
"_PLACEHOLDER",
"time",
"_PLACEHOLDER",
"ip",
"_PLACEHOLDER",
"_PLACEHOLDER",
"extra_info"
]
)
BalanceAvailable = generate_labeler_serializer(
name="BalanceAvailable",
klass=dataclasses.BalanceAvailable,
labels=[
"amount"
]
)
Order = generate_labeler_serializer(
name="Order",
klass=dataclasses.Order,
labels=[
"id",
"gid",
"cid",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"order_type",
"type_prev",
"mts_tif",
"_PLACEHOLDER",
"flags",
"order_status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"price",
"price_avg",
"price_trailing",
"price_aux_limit",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"notify",
"hidden",
"placed_id",
"_PLACEHOLDER",
"_PLACEHOLDER",
"routing",
"_PLACEHOLDER",
"_PLACEHOLDER",
"meta"
]
)
Position = generate_labeler_serializer(
name="Position",
klass=dataclasses.Position,
labels=[
"symbol",
"status",
"amount",
"base_price",
"margin_funding",
"margin_funding_type",
"pl",
"pl_perc",
"price_liq",
"leverage",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"type",
"_PLACEHOLDER",
"collateral",
"collateral_min",
"meta"
]
)
Trade = generate_labeler_serializer(
name="Trade",
klass=dataclasses.Trade,
labels=[
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"order_type",
"order_price",
"maker",
"fee",
"fee_currency",
"cid"
]
)
FundingTrade = generate_labeler_serializer(
name="FundingTrade",
klass=dataclasses.FundingTrade,
labels=[
"id",
"currency",
"mts_create",
"offer_id",
"amount",
"rate",
"period"
]
)
OrderTrade = generate_labeler_serializer(
name="OrderTrade",
klass=dataclasses.OrderTrade,
labels=[
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"_PLACEHOLDER",
"_PLACEHOLDER",
"maker",
"fee",
"fee_currency",
"cid"
]
)
Ledger = generate_labeler_serializer(
name="Ledger",
klass=dataclasses.Ledger,
labels=[
"id",
"currency",
"_PLACEHOLDER",
"mts",
"_PLACEHOLDER",
"amount",
"balance",
"_PLACEHOLDER",
"description"
]
)
FundingOffer = generate_labeler_serializer(
name="FundingOffer",
klass=dataclasses.FundingOffer,
labels=[
"id",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"offer_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"flags",
"offer_status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"rate",
"period",
"notify",
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER"
]
)
FundingCredit = generate_labeler_serializer(
name="FundingCredit",
klass=dataclasses.FundingCredit,
labels=[
"id",
"symbol",
"side",
"mts_create",
"mts_update",
"amount",
"flags",
"status",
"rate_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"rate",
"period",
"mts_opening",
"mts_last_payout",
"notify",
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER",
"no_close",
"position_pair"
]
)
FundingLoan = generate_labeler_serializer(
name="FundingLoan",
klass=dataclasses.FundingLoan,
labels=[
"id",
"symbol",
"side",
"mts_create",
"mts_update",
"amount",
"flags",
"status",
"rate_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"rate",
"period",
"mts_opening",
"mts_last_payout",
"notify",
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER",
"no_close"
]
)
FundingAutoRenew = generate_labeler_serializer(
name="FundingAutoRenew",
klass=dataclasses.FundingAutoRenew,
labels=[
"currency",
"period",
"rate",
"threshold"
]
)
FundingInfo = generate_labeler_serializer(
name="FundingInfo",
klass=dataclasses.FundingInfo,
labels=[
"yield_loan",
"yield_lend",
"duration_loan",
"duration_lend"
]
)
Wallet = generate_labeler_serializer(
name="Wallet",
klass=dataclasses.Wallet,
labels=[
"wallet_type",
"currency",
"balance",
"unsettled_interest",
"available_balance",
"last_change",
"trade_details"
]
)
Transfer = generate_labeler_serializer(
name="Transfer",
klass=dataclasses.Transfer,
labels=[
"mts",
"wallet_from",
"wallet_to",
"_PLACEHOLDER",
"currency",
"currency_to",
"_PLACEHOLDER",
"amount"
]
)
Withdrawal = generate_labeler_serializer(
name="Withdrawal",
klass=dataclasses.Withdrawal,
labels=[
"withdrawal_id",
"_PLACEHOLDER",
"method",
"payment_id",
"wallet",
"amount",
"_PLACEHOLDER",
"_PLACEHOLDER",
"withdrawal_fee"
]
)
DepositAddress = generate_labeler_serializer(
name="DepositAddress",
klass=dataclasses.DepositAddress,
labels=[
"_PLACEHOLDER",
"method",
"currency_code",
"_PLACEHOLDER",
"address",
"pool_address"
]
)
LightningNetworkInvoice = generate_labeler_serializer(
name="LightningNetworkInvoice",
klass=dataclasses.LightningNetworkInvoice,
labels=[
"invoice_hash",
"invoice",
"_PLACEHOLDER",
"_PLACEHOLDER",
"amount"
]
)
Movement = generate_labeler_serializer(
name="Movement",
klass=dataclasses.Movement,
labels=[
"id",
"currency",
"currency_name",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mts_start",
"mts_update",
"_PLACEHOLDER",
"_PLACEHOLDER",
"status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"amount",
"fees",
"_PLACEHOLDER",
"_PLACEHOLDER",
"destination_address",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"transaction_id",
"withdraw_transaction_note"
]
)
SymbolMarginInfo = generate_labeler_serializer(
name="SymbolMarginInfo",
klass=dataclasses.SymbolMarginInfo,
labels=[
"_PLACEHOLDER",
"symbol",
"tradable_balance",
"gross_balance",
"buy",
"sell"
],
flat=True
)
BaseMarginInfo = generate_labeler_serializer(
name="BaseMarginInfo",
klass=dataclasses.BaseMarginInfo,
labels=[
"user_pl",
"user_swaps",
"margin_balance",
"margin_net",
"margin_min"
]
)
PositionClaim = generate_labeler_serializer(
name="PositionClaim",
klass=dataclasses.PositionClaim,
labels=[
"symbol",
"position_status",
"amount",
"base_price",
"margin_funding",
"margin_funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"pos_type",
"_PLACEHOLDER",
"collateral",
"min_collateral",
"meta"
]
)
PositionIncreaseInfo = generate_labeler_serializer(
name="PositionIncreaseInfo",
klass=dataclasses.PositionIncreaseInfo,
labels=[
"max_pos",
"current_pos",
"base_currency_balance",
"tradable_balance_quote_currency",
"tradable_balance_quote_total",
"tradable_balance_base_currency",
"tradable_balance_base_total",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"funding_avail",
"_PLACEHOLDER",
"_PLACEHOLDER",
"funding_value",
"funding_required",
"funding_value_currency",
"funding_required_currency"
],
flat=True
)
PositionIncrease = generate_labeler_serializer(
name="PositionIncrease",
klass=dataclasses.PositionIncrease,
labels=[
"symbol",
"_PLACEHOLDER",
"amount",
"base_price"
]
)
PositionHistory = generate_labeler_serializer(
name="PositionHistory",
klass=dataclasses.PositionHistory,
labels=[
"symbol",
"status",
"amount",
"base_price",
"funding",
"funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update"
]
)
PositionSnapshot = generate_labeler_serializer(
name="PositionSnapshot",
klass=dataclasses.PositionSnapshot,
labels=[
"symbol",
"status",
"amount",
"base_price",
"funding",
"funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update"
]
)
PositionAudit = generate_labeler_serializer(
name="PositionAudit",
klass=dataclasses.PositionAudit,
labels=[
"symbol",
"status",
"amount",
"base_price",
"funding",
"funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"type",
"_PLACEHOLDER",
"collateral",
"collateral_min",
"meta"
]
)
DerivativePositionCollateral = generate_labeler_serializer(
name="DerivativePositionCollateral",
klass=dataclasses.DerivativePositionCollateral,
labels=[
"status"
]
)
DerivativePositionCollateralLimits = generate_labeler_serializer(
name="DerivativePositionCollateralLimits",
klass=dataclasses.DerivativePositionCollateralLimits,
labels=[
"min_collateral",
"max_collateral"
]
)
#endregion

5
bfxapi/urls.py Normal file
View File

@@ -0,0 +1,5 @@
REST_HOST = "https://api.bitfinex.com/v2"
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"

View File

@@ -1 +0,0 @@
NAME = 'utils'

View File

@@ -1,63 +0,0 @@
"""
This module is used to house all of the functions which are used
to handle the http authentication of the client
"""
import hashlib
import hmac
import time
from ..models import Order
def generate_auth_payload(API_KEY, API_SECRET):
"""
Generate a signed payload
@return json Object headers
"""
nonce = _gen_nonce()
authMsg, sig = _gen_signature(API_KEY, API_SECRET, nonce)
return {
'apiKey': API_KEY,
'authSig': sig,
'authNonce': nonce,
'authPayload': authMsg,
'event': 'auth'
}
def generate_auth_headers(API_KEY, API_SECRET, path, body):
"""
Generate headers for a signed payload
"""
nonce = str(_gen_nonce())
signature = "/api/v2/{}{}{}".format(path, nonce, body)
h = hmac.new(API_SECRET.encode('utf8'), signature.encode('utf8'), hashlib.sha384)
signature = h.hexdigest()
return {
"bfx-nonce": nonce,
"bfx-apikey": API_KEY,
"bfx-signature": signature
}
def _gen_signature(API_KEY, API_SECRET, nonce):
authMsg = 'AUTH{}'.format(nonce)
secret = API_SECRET.encode('utf8')
sig = hmac.new(secret, authMsg.encode('utf8'), hashlib.sha384).hexdigest()
return authMsg, sig
def _gen_nonce():
return int(round(time.time() * 1000000))
def gen_unique_cid():
return int(round(time.time() * 1000))
def calculate_order_flags(hidden, close, reduce_only, post_only, oco):
flags = 0
flags = flags + Order.Flags.HIDDEN if hidden else flags
flags = flags + Order.Flags.CLOSE if close else flags
flags = flags + Order.Flags.REDUCE_ONLY if reduce_only else flags
flags = flags + Order.Flags.POST_ONLY if post_only else flags
flags = flags + Order.Flags.OCO if oco else flags
return flags

View File

@@ -1,100 +0,0 @@
"""
Module used to describe all of the different data types
"""
import logging
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
UNDERLINE_SEQ = "\033[04m"
YELLOW = '\033[93m'
WHITE = '\33[37m'
BLUE = '\033[34m'
LIGHT_BLUE = '\033[94m'
RED = '\033[91m'
GREY = '\33[90m'
KEYWORD_COLORS = {
'WARNING': YELLOW,
'INFO': LIGHT_BLUE,
'DEBUG': WHITE,
'CRITICAL': YELLOW,
'ERROR': RED,
'TRADE': '\33[102m\33[30m'
}
def formatter_message(message, use_color = True):
"""
Syntax highlight certain keywords
"""
if use_color:
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
else:
message = message.replace("$RESET", "").replace("$BOLD", "")
return message
def format_word(message, word, color_seq, bold=False, underline=False):
"""
Surround the given word with a sequence
"""
replacer = color_seq + word + RESET_SEQ
if underline:
replacer = UNDERLINE_SEQ + replacer
if bold:
replacer = BOLD_SEQ + replacer
return message.replace(word, replacer)
class Formatter(logging.Formatter):
"""
This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG'
"""
def __init__(self, msg, use_color = True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
"""
Format and highlight certain keywords
"""
levelname = record.levelname
if self.use_color and levelname in KEYWORD_COLORS:
levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ
record.levelname = levelname_color
record.name = GREY + record.name + RESET_SEQ
return logging.Formatter.format(self, record)
class CustomLogger(logging.Logger):
"""
This adds extra logging functions such as logger.trade and also
sets the logger to use the custom formatter
"""
FORMAT = "[$BOLD%(name)s$RESET] [%(levelname)s] %(message)s"
COLOR_FORMAT = formatter_message(FORMAT, True)
TRADE = 50
def __init__(self, name, logLevel='DEBUG'):
logging.Logger.__init__(self, name, logLevel)
color_formatter = Formatter(self.COLOR_FORMAT)
console = logging.StreamHandler()
console.setFormatter(color_formatter)
self.addHandler(console)
logging.addLevelName(self.TRADE, "TRADE")
return
def set_level(self, level):
logging.Logger.setLevel(self, level)
def trade(self, message, *args, **kws):
"""
Print a syntax highlighted trade signal
"""
if self.isEnabledFor(self.TRADE):
message = format_word(message, 'CLOSED ', YELLOW, bold=True)
message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True)
message = format_word(message, 'UPDATED ', BLUE, bold=True)
message = format_word(message, 'CLOSED_ALL ', RED, bold=True)
# Yes, logger takes its '*args' as 'args'.
self._log(self.TRADE, message, args, **kws)

View File

@@ -1,52 +0,0 @@
import decimal as dec
class Decimal(dec.Decimal):
@classmethod
def from_float(cls, f):
return cls(str(f))
def __new__(cls, value=0, *args, **kwargs):
if isinstance(value, float):
value = Decimal.from_float(value)
return super(Decimal, cls).__new__(cls, value, *args, **kwargs)
def __mul__(self, rhs):
if isinstance(rhs, float):
rhs = Decimal.from_float(rhs)
return Decimal(super().__mul__(rhs))
def __rmul__(self, lhs):
return self.__mul__(lhs)
def __add__(self, rhs):
if isinstance(rhs, float):
rhs = Decimal.from_float(rhs)
return Decimal(super().__add__(rhs))
def __radd__(self, lhs):
return self.__add__(lhs)
def __sub__(self, rhs):
if isinstance(rhs, float):
rhs = Decimal.from_float(rhs)
return Decimal(super().__sub__(rhs))
def __rsub__(self, lhs):
return self.__sub__(lhs)
def __truediv__(self, rhs):
if isinstance(rhs, float):
rhs = Decimal.from_float(rhs)
return Decimal(super().__truediv__(rhs))
def __rtruediv__(self, rhs):
return self.__truediv__(rhs)
def __floordiv__(self, rhs):
if isinstance(rhs, float):
rhs = Decimal.from_float(rhs)
return Decimal(super().__floordiv__(rhs))
def __rfloordiv__ (self, rhs):
return self.__floordiv__(rhs)

View File

@@ -1,12 +0,0 @@
from ..utils.custom_logger import CustomLogger
def handle_failure(func):
async def inner_function(*args, **kwargs):
logger = CustomLogger('BfxWebsocket', logLevel="DEBUG")
try:
await func(*args, **kwargs)
except Exception as exception_message:
logger.error(exception_message)
return inner_function

View File

@@ -0,0 +1,31 @@
import json
from decimal import Decimal
from datetime import datetime
from typing import Type, List, Dict, Union, Any
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
def _strip(dictionary: Dict) -> Dict:
return { key: value for key, value in dictionary.items() if value is not None }
def _convert_float_to_str(data: JSON) -> JSON:
if isinstance(data, float):
return format(Decimal(repr(data)), "f")
if isinstance(data, list):
return [ _convert_float_to_str(sub_data) for sub_data in data ]
if isinstance(data, dict):
return _strip({ key: _convert_float_to_str(value) for key, value in data.items() })
return data
class JSONEncoder(json.JSONEncoder):
def encode(self, o: JSON) -> str:
return json.JSONEncoder.encode(self, _convert_float_to_str(o))
def default(self, o: Any) -> Any:
if isinstance(o, Decimal):
return format(o, "f")
if isinstance(o, datetime):
return str(o)
return json.JSONEncoder.default(self, o)

51
bfxapi/utils/logger.py Normal file
View File

@@ -0,0 +1,51 @@
import logging, sys
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
COLOR_SEQ, ITALIC_COLOR_SEQ = "\033[1;%dm", "\033[3;%dm"
COLORS = {
"DEBUG": CYAN,
"INFO": BLUE,
"WARNING": YELLOW,
"ERROR": RED
}
RESET_SEQ = "\033[0m"
class _ColorFormatter(logging.Formatter):
def __init__(self, msg, use_color = True):
logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S")
self.use_color = use_color
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ
record.levelname = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
return logging.Formatter.format(self, record)
class ColorLogger(logging.Logger):
FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s"
def __init__(self, name, level):
logging.Logger.__init__(self, name, level)
colored_formatter = _ColorFormatter(self.FORMAT, use_color=True)
handler = logging.StreamHandler(stream=sys.stderr)
handler.setFormatter(fmt=colored_formatter)
self.addHandler(hdlr=handler)
class FileLogger(logging.Logger):
FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s"
def __init__(self, name, level, filename):
logging.Logger.__init__(self, name, level)
formatter = logging.Formatter(self.FORMAT)
handler = logging.FileHandler(filename=filename)
handler.setFormatter(fmt=formatter)
self.addHandler(hdlr=handler)

View File

@@ -1,87 +0,0 @@
import time
import json
import asyncio
from .. import Client, BfxWebsocket
def get_now():
return int(round(time.time() * 1000))
class StubbedWebsocket(BfxWebsocket):
def __new__(cls, *args, **kwargs):
instance = super(StubbedWebsocket, cls).__new__(cls, *args, **kwargs)
instance.sent_items = []
instance.published_items = []
return instance
async def _main(self, host):
print ("Faking wesocket connection to {}".format(host))
async def publish(self, data, is_json=True):
self.published_items += [{
'time': get_now(),
'data': data
}]
# convert to string and push through the websocket
data = json.dumps(data) if is_json else data
return await self.on_message(data)
async def publish_auth_confirmation(self):
return self.publish({"event":"auth","status":"OK","chanId":0,"userId":269499,"auth_id":"58aa0472-b1a9-4690-8ab8-300d68e66aaf","caps":{"orders":{"read":1,"write":1},"account":{"read":1,"write":0},"funding":{"read":1,"write":1},"history":{"read":1,"write":0},"wallets":{"read":1,"write":1},"withdraw":{"read":0,"write":1},"positions":{"read":1,"write":1}}})
async def send(self, data_string):
self.sent_items += [{
'time': get_now(),
'data': data_string
}]
def get_published_items(self):
return self.published_items
def get_sent_items(self):
return self.sent_items
def get_last_sent_item(self):
return self.sent_items[-1:][0]
def get_sent_items_count(self):
return len(self.sent_items)
class EventWatcher():
def __init__(self, ws, event):
self.value = None
self.event = event
ws.once(event, self._finish)
def _finish(self, value):
self.value = value or {}
@classmethod
def watch(cls, ws, event):
return EventWatcher(ws, event)
def wait_until_complete(self, max_wait_time=5):
counter = 0
while self.value == None:
if counter > 5:
raise Exception('Wait time limit exceeded for event {}'.format(self.event))
time.sleep(1)
counter += 1
return self.value
def create_stubbed_client(*args, **kwargs):
client = Client(*args, **kwargs)
# no support for rest stubbing yet
client.rest = None
client.ws = StubbedWebsocket(*args, **kwargs)
return client
async def ws_publish_auth_accepted(ws):
return await ws.publish({"event":"auth","status":"OK","chanId":0,"userId":269499,"auth_id":"58aa0472-b1a9-4690-8ab8-300d68e66aaf","caps":{"orders":{"read":1,"write":1},"account":{"read":1,"write":0},"funding":{"read":1,"write":1},"history":{"read":1,"write":0},"wallets":{"read":1,"write":1},"withdraw":{"read":0,"write":1},"positions":{"read":1,"write":1}}})
async def ws_publish_connection_init(ws):
return await ws.publish({"event":"info","version":2,"serverId":"748c00f2-250b-46bb-8519-ce1d7d68e4f0","platform":{"status":1}})
async def ws_publish_conf_accepted(ws, flags_code):
return await ws.publish({"event":"conf","status":"OK","flags":flags_code})

View File

@@ -1,5 +1 @@
"""
This module contains the current version of the bfxapi lib
"""
__version__ = '2.0.6'
__version__ = "3.0.0b2"

View File

@@ -0,0 +1 @@
from .client import BfxWebSocketClient, BfxWebSocketBucket, BfxWebSocketInputs

View File

@@ -0,0 +1,3 @@
from .bfx_websocket_client import BfxWebSocketClient
from .bfx_websocket_bucket import BfxWebSocketBucket
from .bfx_websocket_inputs import BfxWebSocketInputs

Some files were not shown because too many files have changed in this diff Show More