mirror of
https://github.com/aljazceru/bitfinex-api-py.git
synced 2025-12-18 22:34:21 +01:00
Merge branch v3-beta in branch master.
This commit is contained in:
13
.github/ISSUE_TEMPLATE
vendored
13
.github/ISSUE_TEMPLATE
vendored
@@ -1,13 +0,0 @@
|
||||
#### Issue type
|
||||
- [ ] bug
|
||||
- [ ] missing functionality
|
||||
- [ ] performance
|
||||
- [ ] feature request
|
||||
|
||||
#### Brief description
|
||||
|
||||
#### Steps to reproduce
|
||||
-
|
||||
|
||||
##### Additional Notes:
|
||||
-
|
||||
15
.github/PULL_REQUEST_TEMPLATE
vendored
15
.github/PULL_REQUEST_TEMPLATE
vendored
@@ -1,15 +0,0 @@
|
||||
### Description:
|
||||
...
|
||||
|
||||
### Breaking changes:
|
||||
- [ ]
|
||||
|
||||
### New features:
|
||||
- [ ]
|
||||
|
||||
### Fixes:
|
||||
- [ ]
|
||||
|
||||
### PR status:
|
||||
- [ ] Version bumped
|
||||
- [ ] Change-log updated
|
||||
36
.github/workflows/python-app.yml
vendored
36
.github/workflows/python-app.yml
vendored
@@ -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
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
.vscode
|
||||
*.log
|
||||
dist
|
||||
bitfinex_api_py.egg-info
|
||||
__pycache__
|
||||
|
||||
dist
|
||||
venv
|
||||
!.gitkeep
|
||||
@@ -1,8 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.8.2"
|
||||
install:
|
||||
- python3.8 -m pip install -r requirements.txt
|
||||
script:
|
||||
- pylint --rcfile=pylint.rc bfxapi
|
||||
- pytest
|
||||
172
CHANGELOG
172
CHANGELOG
@@ -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
|
||||
28
LICENSE
28
LICENSE
@@ -1,3 +1,4 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -173,29 +174,4 @@
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
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.
|
||||
END OF TERMS AND CONDITIONS
|
||||
175
README.md
175
README.md
@@ -1,175 +0,0 @@
|
||||
# Bitfinex Trading Library for Python - Bitcoin, Ethereum, Ripple and more
|
||||
|
||||

|
||||
|
||||
A Python reference implementation of the Bitfinex API for both REST and websocket interaction.
|
||||
|
||||
# Features
|
||||
- Official implementation
|
||||
- Websocket V2 and Rest V2
|
||||
- Connection multiplexing
|
||||
- Order and wallet management
|
||||
- All market data feeds
|
||||
|
||||
## Installation
|
||||
|
||||
Clone package into PYTHONPATH:
|
||||
```sh
|
||||
git clone https://github.com/bitfinexcom/bitfinex-api-py.git
|
||||
cd bitfinex-api-py
|
||||
```
|
||||
|
||||
Or via pip:
|
||||
```sh
|
||||
python3 -m pip install bitfinex-api-py
|
||||
```
|
||||
|
||||
Run the trades/candles example:
|
||||
```sh
|
||||
cd bfxapi/examples/ws
|
||||
python3 subscribe_trades_candles.py
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
from bfxapi import Client, Order
|
||||
|
||||
bfx = Client(
|
||||
API_KEY='<YOUR_API_KEY>',
|
||||
API_SECRET='<YOUR_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.ws.run()
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
* <b>[V2 Rest](docs/rest_v2.md)</b> - Documentation
|
||||
* <b>[V2 Websocket](docs/ws_v2.md)</b> - Documentation
|
||||
|
||||
## Examples
|
||||
|
||||
#### Authenticate
|
||||
|
||||
```python
|
||||
bfx = Client(
|
||||
API_KEY='<YOUR_API_KEY>',
|
||||
API_SECRET='<YOUR_API_SECRET>'
|
||||
)
|
||||
|
||||
@bfx.ws.on('authenticated')
|
||||
async def do_something():
|
||||
print ("Success!")
|
||||
|
||||
bfx.ws.run()
|
||||
```
|
||||
|
||||
#### Subscribe to trades
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
#### Withdraw from wallet via REST
|
||||
|
||||
```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)
|
||||
```
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
python setup.py sdist
|
||||
twine upload dist/*
|
||||
```
|
||||
@@ -1,14 +1,6 @@
|
||||
"""
|
||||
This module is used to interact with the bitfinex api
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
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
|
||||
from .urls import REST_HOST, PUB_REST_HOST, STAGING_REST_HOST, \
|
||||
WSS_HOST, PUB_WSS_HOST, STAGING_WSS_HOST
|
||||
|
||||
NAME = 'bfxapi'
|
||||
NAME = "bfxapi"
|
||||
@@ -1,24 +1,31 @@
|
||||
"""
|
||||
This module exposes the core bitfinex clients which includes both
|
||||
a websocket client and a rest interface client
|
||||
"""
|
||||
from .rest import BfxRestInterface
|
||||
from .websocket import BfxWebsocketClient
|
||||
from .urls import REST_HOST, WSS_HOST
|
||||
|
||||
# pylint: disable-all
|
||||
from typing import List, Optional
|
||||
|
||||
from .websockets.bfx_websocket import BfxWebsocket
|
||||
from .rest.bfx_rest import BfxRest
|
||||
from .constants import *
|
||||
class Client(object):
|
||||
def __init__(
|
||||
self,
|
||||
REST_HOST: str = REST_HOST,
|
||||
WSS_HOST: str = WSS_HOST,
|
||||
API_KEY: Optional[str] = None,
|
||||
API_SECRET: Optional[str] = None,
|
||||
filter: Optional[List[str]] = None,
|
||||
log_level: str = "INFO"
|
||||
):
|
||||
credentials = None
|
||||
|
||||
class Client:
|
||||
"""
|
||||
The bfx client exposes rest and websocket objects
|
||||
"""
|
||||
if API_KEY and API_SECRET:
|
||||
credentials = { "API_KEY": API_KEY, "API_SECRET": API_SECRET, "filter": filter }
|
||||
|
||||
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)
|
||||
self.rest = BfxRestInterface(
|
||||
host=REST_HOST,
|
||||
credentials=credentials
|
||||
)
|
||||
|
||||
self.wss = BfxWebsocketClient(
|
||||
host=WSS_HOST,
|
||||
credentials=credentials,
|
||||
log_level=log_level
|
||||
)
|
||||
@@ -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
50
bfxapi/enums.py
Normal 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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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!!")
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
19
bfxapi/exceptions.py
Normal file
19
bfxapi/exceptions.py
Normal file
@@ -0,0 +1,19 @@
|
||||
__all__ = [
|
||||
"BfxBaseException",
|
||||
|
||||
"LabelerSerializerException",
|
||||
]
|
||||
|
||||
class BfxBaseException(Exception):
|
||||
"""
|
||||
Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class LabelerSerializerException(BfxBaseException):
|
||||
"""
|
||||
This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py.
|
||||
"""
|
||||
|
||||
pass
|
||||
77
bfxapi/labeler.py
Normal file
77
bfxapi/labeler.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from .exceptions import LabelerSerializerException
|
||||
|
||||
from typing import Type, Generic, TypeVar, Iterable, Optional, 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(object):
|
||||
"""
|
||||
Base class for any dataclass serializable by the _Serializer generic class.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class _Serializer(Generic[T]):
|
||||
def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]):
|
||||
self.name, self.klass, self.__labels, self.__IGNORE = name, klass, labels, IGNORE
|
||||
|
||||
def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]:
|
||||
labels = list(filter(lambda label: label not in (skip or list()), self.__labels))
|
||||
|
||||
if len(labels) > len(args):
|
||||
raise LabelerSerializerException(f"{self.name} -> <labels> and <*args> arguments should contain the same amount of elements.")
|
||||
|
||||
for index, label in enumerate(labels):
|
||||
if label not in self.__IGNORE:
|
||||
yield label, args[index]
|
||||
|
||||
def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T:
|
||||
return cast(T, self.klass(**dict(self._serialize(*values, skip=skip))))
|
||||
|
||||
def get_labels(self) -> List[str]:
|
||||
return [ label for label in self.__labels if label not in self.__IGNORE ]
|
||||
|
||||
class _RecursiveSerializer(_Serializer, Generic[T]):
|
||||
def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]):
|
||||
super().__init__(name, klass, labels, IGNORE)
|
||||
|
||||
self.serializers = serializers
|
||||
|
||||
def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T:
|
||||
serialization = dict(self._serialize(*values, skip=skip))
|
||||
|
||||
for key in serialization:
|
||||
if key in self.serializers.keys():
|
||||
serialization[key] = self.serializers[key].parse(*serialization[key], skip=skip)
|
||||
|
||||
return cast(T, self.klass(**serialization))
|
||||
|
||||
def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]:
|
||||
return _Serializer[T](name, klass, labels, IGNORE)
|
||||
|
||||
def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]:
|
||||
return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE)
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
38
bfxapi/notification.py
Normal file
38
bfxapi/notification.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List, Dict, Union, Optional, Any, TypedDict, 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, skip: Optional[List[str]] = None) -> Notification[T]:
|
||||
notification = cast(Notification[T], Notification(**dict(self._serialize(*values))))
|
||||
|
||||
if isinstance(self.serializer, _Serializer):
|
||||
data = cast(List[Any], notification.data)
|
||||
|
||||
if self.is_iterable == False:
|
||||
if len(data) == 1 and isinstance(data[0], list):
|
||||
data = data[0]
|
||||
|
||||
notification.data = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))))
|
||||
else: notification.data = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*sub_data, skip=skip))) for sub_data in data ])
|
||||
|
||||
return notification
|
||||
@@ -1 +1,4 @@
|
||||
NAME = 'rest'
|
||||
from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \
|
||||
RestMerchantEndpoints
|
||||
|
||||
NAME = "rest"
|
||||
File diff suppressed because it is too large
Load Diff
7
bfxapi/rest/endpoints/__init__.py
Normal file
7
bfxapi/rest/endpoints/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .bfx_rest_interface import BfxRestInterface
|
||||
|
||||
from .rest_public_endpoints import RestPublicEndpoints
|
||||
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
|
||||
from .rest_merchant_endpoints import RestMerchantEndpoints
|
||||
|
||||
NAME = "endpoints"
|
||||
16
bfxapi/rest/endpoints/bfx_rest_interface.py
Normal file
16
bfxapi/rest/endpoints/bfx_rest_interface.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from .rest_public_endpoints import RestPublicEndpoints
|
||||
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
|
||||
from .rest_merchant_endpoints import RestMerchantEndpoints
|
||||
|
||||
class BfxRestInterface(object):
|
||||
VERSION = 2
|
||||
|
||||
def __init__(self, host, credentials = None):
|
||||
API_KEY, API_SECRET = credentials and \
|
||||
(credentials["API_KEY"], credentials["API_SECRET"]) or (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)
|
||||
321
bfxapi/rest/endpoints/rest_authenticated_endpoints.py
Normal file
321
bfxapi/rest/endpoints/rest_authenticated_endpoints.py
Normal file
@@ -0,0 +1,321 @@
|
||||
from typing import List, Tuple, Union, Literal, Optional
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
|
||||
from .. types import *
|
||||
|
||||
from .. import serializers
|
||||
from .. enums import Sort, OrderType, FundingOfferType
|
||||
from .. middleware import Middleware
|
||||
|
||||
class RestAuthenticatedEndpoints(Middleware):
|
||||
def get_user_info(self) -> UserInfo:
|
||||
return serializers.UserInfo.parse(*self._POST(f"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:
|
||||
return serializers.BalanceAvailable.parse(*self._POST("auth/calc/order/avail", body={
|
||||
"symbol": symbol, "type": type, "dir": dir,
|
||||
"rate": rate, "lev": lev
|
||||
}))
|
||||
|
||||
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]:
|
||||
endpoint = "auth/r/orders"
|
||||
|
||||
if symbol != None:
|
||||
endpoint += f"/{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 serializers._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 serializers._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]:
|
||||
body = {
|
||||
"id": id,
|
||||
"cid": cid,
|
||||
"cid_date": cid_date
|
||||
}
|
||||
|
||||
return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/cancel", body=body))
|
||||
|
||||
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 serializers._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 == 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 == 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(f"auth/r/info/margin/base")[1]))
|
||||
|
||||
def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo:
|
||||
response = self._POST(f"auth/r/info/margin/{symbol}")
|
||||
data = [response[1]] + response[2]
|
||||
return serializers.SymbolMarginInfo.parse(*data)
|
||||
|
||||
def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]:
|
||||
return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"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 serializers._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 serializers._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:
|
||||
response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount })
|
||||
data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5]
|
||||
return serializers.PositionIncreaseInfo.parse(*data)
|
||||
|
||||
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]:
|
||||
return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ]
|
||||
|
||||
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/limits", body={ "symbol": symbol }))
|
||||
|
||||
def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]:
|
||||
endpoint = "auth/r/funding/offers"
|
||||
|
||||
if symbol != None:
|
||||
endpoint += f"/{symbol}"
|
||||
|
||||
return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ]
|
||||
|
||||
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 serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", body=body))
|
||||
|
||||
def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]:
|
||||
return serializers._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 serializers._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 serializers._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]:
|
||||
return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", body={
|
||||
"status": int(status),
|
||||
"currency": currency, "amount": amount,
|
||||
"rate": rate, "period": period
|
||||
}))
|
||||
|
||||
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 serializers._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 == None:
|
||||
endpoint = "auth/r/funding/offers/hist"
|
||||
else: endpoint = f"auth/r/funding/offers/{symbol}/hist"
|
||||
|
||||
body = {
|
||||
"start": start, "end": end,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
|
||||
|
||||
def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]:
|
||||
if symbol == 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 == None:
|
||||
endpoint = "auth/r/funding/loans/hist"
|
||||
else: endpoint = f"auth/r/funding/loans/{symbol}/hist"
|
||||
|
||||
body = {
|
||||
"start": start, "end": end,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
|
||||
|
||||
def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]:
|
||||
if symbol == 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 == None:
|
||||
endpoint = "auth/r/funding/credits/hist"
|
||||
else: endpoint = f"auth/r/funding/credits/{symbol}/hist"
|
||||
|
||||
body = {
|
||||
"start": start, "end": end,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
|
||||
|
||||
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 == 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:
|
||||
response = self._POST(f"auth/r/info/funding/{key}")
|
||||
data = [response[1]] + response[2]
|
||||
return serializers.FundingInfo.parse(*data)
|
||||
|
||||
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 serializers._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]:
|
||||
return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", body={
|
||||
"wallet": wallet, "method": method,
|
||||
"address": address, "amount": amount,
|
||||
}))
|
||||
|
||||
def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]:
|
||||
body = {
|
||||
"wallet": wallet,
|
||||
"method": method,
|
||||
"renew": int(renew)
|
||||
}
|
||||
|
||||
return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body))
|
||||
|
||||
def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice:
|
||||
body = {
|
||||
"wallet": wallet, "currency": currency,
|
||||
"amount": amount
|
||||
}
|
||||
|
||||
return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body))
|
||||
|
||||
def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]:
|
||||
if currency == None:
|
||||
endpoint = "auth/r/movements/hist"
|
||||
else: endpoint = f"auth/r/movements/{currency}/hist"
|
||||
|
||||
body = {
|
||||
"start": start, "end": end,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
|
||||
69
bfxapi/rest/endpoints/rest_merchant_endpoints.py
Normal file
69
bfxapi/rest/endpoints/rest_merchant_endpoints.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from typing import TypedDict, List, Union, Literal, Optional
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .. types import *
|
||||
from .. middleware import Middleware
|
||||
from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys
|
||||
|
||||
_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):
|
||||
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]:
|
||||
return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/invoices", body={
|
||||
"id": id, "start": start, "end": end,
|
||||
"limit": limit
|
||||
})) ]
|
||||
|
||||
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:
|
||||
return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })))
|
||||
|
||||
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
|
||||
}))
|
||||
186
bfxapi/rest/endpoints/rest_public_endpoints.py
Normal file
186
bfxapi/rest/endpoints/rest_public_endpoints.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from typing import List, Union, Literal, Optional, Any, cast
|
||||
from decimal import Decimal
|
||||
|
||||
from .. types import *
|
||||
|
||||
from .. import serializers
|
||||
from .. enums import Config, Sort
|
||||
from .. middleware import Middleware
|
||||
|
||||
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]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]:
|
||||
data = self._GET("tickers", params={ "symbols": ",".join(symbols) })
|
||||
|
||||
parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }
|
||||
|
||||
return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ]
|
||||
|
||||
def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]:
|
||||
if isinstance(pairs, str) and pairs == "ALL":
|
||||
return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ]
|
||||
|
||||
data = self.get_tickers([ pair for pair in pairs ])
|
||||
|
||||
return cast(List[TradingPairTicker], data)
|
||||
|
||||
def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]:
|
||||
if isinstance(currencies, str) and currencies == "ALL":
|
||||
return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ]
|
||||
|
||||
data = self.get_tickers([ currency for currency in currencies ])
|
||||
|
||||
return cast(List[FundingCurrencyTicker], data)
|
||||
|
||||
def get_t_ticker(self, pair: str) -> TradingPairTicker:
|
||||
return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"])
|
||||
|
||||
def get_f_ticker(self, currency: str) -> FundingCurrencyTicker:
|
||||
return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["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"]]) -> List[DerivativesStatus]:
|
||||
if keys == "ALL":
|
||||
params = { "keys": "ALL" }
|
||||
else: params = { "keys": ",".join(keys) }
|
||||
|
||||
data = self._GET(f"status/deriv", params=params)
|
||||
|
||||
return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ]
|
||||
|
||||
def get_derivatives_status_history(
|
||||
self,
|
||||
type: str, symbol: 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/{type}/{symbol}/hist", params=params)
|
||||
return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) 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?limit={limit}&start={start}&end={end}&sort={sort}", 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(self, nickname: str) -> PulseProfile:
|
||||
return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}"))
|
||||
|
||||
def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]:
|
||||
messages = list()
|
||||
|
||||
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 }))
|
||||
36
bfxapi/rest/enums.py
Normal file
36
bfxapi/rest/enums.py
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
45
bfxapi/rest/exceptions.py
Normal file
45
bfxapi/rest/exceptions.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from .. exceptions import BfxBaseException
|
||||
|
||||
__all__ = [
|
||||
"BfxRestException",
|
||||
|
||||
"ResourceNotFound",
|
||||
"RequestParametersError",
|
||||
"ResourceNotFound",
|
||||
"InvalidAuthenticationCredentials"
|
||||
]
|
||||
|
||||
class BfxRestException(BfxBaseException):
|
||||
"""
|
||||
Base class for all custom exceptions in bfxapi/rest/exceptions.py.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class ResourceNotFound(BfxRestException):
|
||||
"""
|
||||
This error indicates a failed HTTP request to a non-existent resource.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class RequestParametersError(BfxRestException):
|
||||
"""
|
||||
This error indicates that there are some invalid parameters sent along with an HTTP request.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class InvalidAuthenticationCredentials(BfxRestException):
|
||||
"""
|
||||
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class UnknownGenericError(BfxRestException):
|
||||
"""
|
||||
This error indicates an undefined problem processing an HTTP request sent to the APIs.
|
||||
"""
|
||||
|
||||
pass
|
||||
3
bfxapi/rest/middleware/__init__.py
Normal file
3
bfxapi/rest/middleware/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .middleware import Middleware
|
||||
|
||||
NAME = "middleware"
|
||||
82
bfxapi/rest/middleware/middleware.py
Normal file
82
bfxapi/rest/middleware/middleware.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import time, hmac, hashlib, json, requests
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Any, cast
|
||||
|
||||
from http import HTTPStatus
|
||||
from ..enums import Error
|
||||
from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError
|
||||
|
||||
from ...utils.JSONEncoder import JSONEncoder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from requests.sessions import _Params
|
||||
|
||||
class Middleware(object):
|
||||
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 = int(round(time.time() * 1_000_000))
|
||||
|
||||
if data == 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(f"{self.host}/{endpoint}", params=params)
|
||||
|
||||
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(f"The request was rejected with the following parameter error: <{data[2]}>")
|
||||
|
||||
if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
|
||||
raise UnknownGenericError(f"The server replied to the request with 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 _ignore_authentication_headers == False:
|
||||
headers = { **headers, **self.__build_authentication_headers(endpoint, data) }
|
||||
|
||||
response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers)
|
||||
|
||||
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(f"The request was rejected with the 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] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
|
||||
raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.")
|
||||
|
||||
return data
|
||||
749
bfxapi/rest/serializers.py
Normal file
749
bfxapi/rest/serializers.py
Normal file
@@ -0,0 +1,749 @@
|
||||
from . import types
|
||||
|
||||
from .. labeler import generate_labeler_serializer, generate_recursive_serializer
|
||||
|
||||
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 Serializers definition for Rest Public Endpoints
|
||||
|
||||
PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[
|
||||
"status"
|
||||
])
|
||||
|
||||
TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[
|
||||
"symbol",
|
||||
"bid",
|
||||
"bid_size",
|
||||
"ask",
|
||||
"ask_size",
|
||||
"daily_change",
|
||||
"daily_change_relative",
|
||||
"last_price",
|
||||
"volume",
|
||||
"high",
|
||||
"low"
|
||||
])
|
||||
|
||||
FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[
|
||||
"symbol",
|
||||
"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("TickersHistory", klass=types.TickersHistory, labels=[
|
||||
"symbol",
|
||||
"bid",
|
||||
"_PLACEHOLDER",
|
||||
"ask",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"mts"
|
||||
])
|
||||
|
||||
TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[
|
||||
"id",
|
||||
"mts",
|
||||
"amount",
|
||||
"price"
|
||||
])
|
||||
|
||||
FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[
|
||||
"id",
|
||||
"mts",
|
||||
"amount",
|
||||
"rate",
|
||||
"period"
|
||||
])
|
||||
|
||||
TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[
|
||||
"price",
|
||||
"count",
|
||||
"amount"
|
||||
])
|
||||
|
||||
FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[
|
||||
"rate",
|
||||
"period",
|
||||
"count",
|
||||
"amount"
|
||||
])
|
||||
|
||||
TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[
|
||||
"order_id",
|
||||
"price",
|
||||
"amount"
|
||||
])
|
||||
|
||||
FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[
|
||||
"offer_id",
|
||||
"period",
|
||||
"rate",
|
||||
"amount"
|
||||
])
|
||||
|
||||
Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[
|
||||
"mts",
|
||||
"value"
|
||||
])
|
||||
|
||||
Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[
|
||||
"mts",
|
||||
"open",
|
||||
"close",
|
||||
"high",
|
||||
"low",
|
||||
"volume"
|
||||
])
|
||||
|
||||
DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[
|
||||
"key",
|
||||
"mts",
|
||||
"_PLACEHOLDER",
|
||||
"deriv_price",
|
||||
"spot_price",
|
||||
"_PLACEHOLDER",
|
||||
"insurance_fund_balance",
|
||||
"_PLACEHOLDER",
|
||||
"next_funding_evt_timestamp_ms",
|
||||
"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("Liquidation", klass=types.Liquidation, labels=[
|
||||
"_PLACEHOLDER",
|
||||
"pos_id",
|
||||
"mts",
|
||||
"_PLACEHOLDER",
|
||||
"symbol",
|
||||
"amount",
|
||||
"base_price",
|
||||
"_PLACEHOLDER",
|
||||
"is_match",
|
||||
"is_market_sold",
|
||||
"_PLACEHOLDER",
|
||||
"price_acquired"
|
||||
])
|
||||
|
||||
Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[
|
||||
"mts",
|
||||
"_PLACEHOLDER",
|
||||
"username",
|
||||
"ranking",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"value",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"twitter_handle"
|
||||
])
|
||||
|
||||
FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[
|
||||
"timestamp",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"frr",
|
||||
"avg_period",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"funding_amount",
|
||||
"funding_amount_used",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"funding_below_threshold"
|
||||
])
|
||||
|
||||
PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.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("PulseMessage", klass=types.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("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[
|
||||
"price_avg",
|
||||
"amount"
|
||||
])
|
||||
|
||||
FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[
|
||||
"rate_avg",
|
||||
"amount"
|
||||
])
|
||||
|
||||
FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[
|
||||
"current_rate"
|
||||
])
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serializers definition for Rest Authenticated Endpoints
|
||||
|
||||
UserInfo = generate_labeler_serializer("UserInfo", klass=types.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("LoginHistory", klass=types.LoginHistory, labels=[
|
||||
"id",
|
||||
"_PLACEHOLDER",
|
||||
"time",
|
||||
"_PLACEHOLDER",
|
||||
"ip",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"extra_info"
|
||||
])
|
||||
|
||||
BalanceAvailable = generate_labeler_serializer("BalanceAvailable", klass=types.BalanceAvailable, labels=[
|
||||
"amount"
|
||||
])
|
||||
|
||||
Order = generate_labeler_serializer("Order", klass=types.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("Position", klass=types.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("Trade", klass=types.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("FundingTrade", klass=types.FundingTrade, labels=[
|
||||
"id",
|
||||
"currency",
|
||||
"mts_create",
|
||||
"offer_id",
|
||||
"amount",
|
||||
"rate",
|
||||
"period"
|
||||
])
|
||||
|
||||
OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[
|
||||
"id",
|
||||
"symbol",
|
||||
"mts_create",
|
||||
"order_id",
|
||||
"exec_amount",
|
||||
"exec_price",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"maker",
|
||||
"fee",
|
||||
"fee_currency",
|
||||
"cid"
|
||||
])
|
||||
|
||||
Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[
|
||||
"id",
|
||||
"currency",
|
||||
"_PLACEHOLDER",
|
||||
"mts",
|
||||
"_PLACEHOLDER",
|
||||
"amount",
|
||||
"balance",
|
||||
"_PLACEHOLDER",
|
||||
"description"
|
||||
])
|
||||
|
||||
FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.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("FundingCredit", klass=types.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("FundingLoan", klass=types.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("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[
|
||||
"currency",
|
||||
"period",
|
||||
"rate",
|
||||
"threshold"
|
||||
])
|
||||
|
||||
FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo, labels=[
|
||||
"symbol",
|
||||
"yield_loan",
|
||||
"yield_lend",
|
||||
"duration_loan",
|
||||
"duration_lend"
|
||||
])
|
||||
|
||||
Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[
|
||||
"wallet_type",
|
||||
"currency",
|
||||
"balance",
|
||||
"unsettled_interest",
|
||||
"available_balance",
|
||||
"last_change",
|
||||
"trade_details"
|
||||
])
|
||||
|
||||
Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[
|
||||
"mts",
|
||||
"wallet_from",
|
||||
"wallet_to",
|
||||
"_PLACEHOLDER",
|
||||
"currency",
|
||||
"currency_to",
|
||||
"_PLACEHOLDER",
|
||||
"amount"
|
||||
])
|
||||
|
||||
Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[
|
||||
"withdrawal_id",
|
||||
"_PLACEHOLDER",
|
||||
"method",
|
||||
"payment_id",
|
||||
"wallet",
|
||||
"amount",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"withdrawal_fee"
|
||||
])
|
||||
|
||||
DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[
|
||||
"_PLACEHOLDER",
|
||||
"method",
|
||||
"currency_code",
|
||||
"_PLACEHOLDER",
|
||||
"address",
|
||||
"pool_address"
|
||||
])
|
||||
|
||||
LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[
|
||||
"invoice_hash",
|
||||
"invoice",
|
||||
"_PLACEHOLDER",
|
||||
"_PLACEHOLDER",
|
||||
"amount"
|
||||
])
|
||||
|
||||
Movement = generate_labeler_serializer("Movement", klass=types.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("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[
|
||||
"symbol",
|
||||
"tradable_balance",
|
||||
"gross_balance",
|
||||
"buy",
|
||||
"sell"
|
||||
])
|
||||
|
||||
BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[
|
||||
"user_pl",
|
||||
"user_swaps",
|
||||
"margin_balance",
|
||||
"margin_net",
|
||||
"margin_min"
|
||||
])
|
||||
|
||||
PositionClaim = generate_labeler_serializer("PositionClaim", klass=types.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("PositionIncreaseInfo", klass=types.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",
|
||||
"funding_value",
|
||||
"funding_required",
|
||||
"funding_value_currency",
|
||||
"funding_required_currency"
|
||||
])
|
||||
|
||||
PositionIncrease = generate_labeler_serializer("PositionIncrease", klass=types.PositionIncrease, labels=[
|
||||
"symbol",
|
||||
"_PLACEHOLDER",
|
||||
"amount",
|
||||
"base_price"
|
||||
])
|
||||
|
||||
PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.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("PositionSnapshot", klass=types.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("PositionAudit", klass=types.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("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[
|
||||
"status"
|
||||
])
|
||||
|
||||
DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[
|
||||
"min_collateral",
|
||||
"max_collateral"
|
||||
])
|
||||
|
||||
#endregion
|
||||
652
bfxapi/rest/types.py
Normal file
652
bfxapi/rest/types.py
Normal file
@@ -0,0 +1,652 @@
|
||||
from typing import *
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .. labeler import _Type, partial, compose
|
||||
from .. notification import Notification
|
||||
from .. utils.JSONEncoder import JSON
|
||||
|
||||
#region Type hinting for Rest Public Endpoints
|
||||
|
||||
@dataclass
|
||||
class PlatformStatus(_Type):
|
||||
status: int
|
||||
|
||||
@dataclass
|
||||
class TradingPairTicker(_Type):
|
||||
symbol: Optional[str]
|
||||
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):
|
||||
symbol: Optional[str]
|
||||
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: float
|
||||
close: float
|
||||
high: float
|
||||
low: float
|
||||
volume: float
|
||||
|
||||
@dataclass
|
||||
class DerivativesStatus(_Type):
|
||||
key: Optional[str]
|
||||
mts: int
|
||||
deriv_price: float
|
||||
spot_price: float
|
||||
insurance_fund_balance: float
|
||||
next_funding_evt_timestamp_ms: 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):
|
||||
timestamp: 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 Type hinting for Rest Authenticated Endpoints
|
||||
|
||||
@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):
|
||||
symbol: str
|
||||
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 Type hinting for Rest Merchant Endpoints
|
||||
|
||||
@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"] != 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"] != None:
|
||||
data["payment"] = InvoiceSubmission.Payment(**data["payment"])
|
||||
|
||||
if "additional_payments" in data and data["additional_payments"] != 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 InvoiceStats(_Type):
|
||||
time: str
|
||||
count: float
|
||||
|
||||
@dataclass
|
||||
class CurrencyConversion(_Type):
|
||||
base_currency: str
|
||||
convert_currency: str
|
||||
created: int
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,18 @@
|
||||
import unittest
|
||||
from .test_rest_serializers import TestRestSerializers
|
||||
from .test_websocket_serializers import TestWebsocketSerializers
|
||||
from .test_labeler import TestLabeler
|
||||
from .test_notification import TestNotification
|
||||
|
||||
NAME = "tests"
|
||||
|
||||
def suite():
|
||||
return unittest.TestSuite([
|
||||
unittest.makeSuite(TestRestSerializers),
|
||||
unittest.makeSuite(TestWebsocketSerializers),
|
||||
unittest.makeSuite(TestLabeler),
|
||||
unittest.makeSuite(TestNotification),
|
||||
])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.TextTestRunner().run(suite())
|
||||
@@ -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})
|
||||
@@ -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"
|
||||
56
bfxapi/tests/test_labeler.py
Normal file
56
bfxapi/tests/test_labeler.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import unittest
|
||||
|
||||
from dataclasses import dataclass
|
||||
from ..exceptions import LabelerSerializerException
|
||||
from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer
|
||||
|
||||
class TestLabeler(unittest.TestCase):
|
||||
def test_generate_labeler_serializer(self):
|
||||
@dataclass
|
||||
class Test(_Type):
|
||||
A: 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.assertEqual(serializer.parse(5, 65.0, "X", skip=[ "_PLACEHOLDER" ]), Test(5, 65.0, "X"),
|
||||
msg="_Serializer should produce the right result when skip parameter is given.")
|
||||
|
||||
self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ],
|
||||
msg="_Serializer::get_labels() should return the right list of labels.")
|
||||
|
||||
with self.assertRaises(LabelerSerializerException,
|
||||
msg="_Serializer should raise LabelerSerializerException 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"], { "E": inner })
|
||||
outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], { "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()
|
||||
25
bfxapi/tests/test_notification.py
Normal file
25
bfxapi/tests/test_notification.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import unittest
|
||||
|
||||
from dataclasses import dataclass
|
||||
from ..labeler import generate_labeler_serializer
|
||||
from ..notification import _Type, _Notification, Notification
|
||||
|
||||
class TestNotification(unittest.TestCase):
|
||||
def test_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)
|
||||
|
||||
self.assertEqual(notification.parse(*[1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], 0, "SUCCESS", "This is just a test notification."]),
|
||||
Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), 0, "SUCCESS", "This is just a test notification."),
|
||||
msg="_Notification should produce the right notification.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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)
|
||||
17
bfxapi/tests/test_rest_serializers.py
Normal file
17
bfxapi/tests/test_rest_serializers.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import unittest
|
||||
|
||||
from ..labeler import _Type
|
||||
|
||||
from ..rest import serializers
|
||||
|
||||
class TestRestSerializers(unittest.TestCase):
|
||||
def test_rest_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 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()
|
||||
17
bfxapi/tests/test_websocket_serializers.py
Normal file
17
bfxapi/tests/test_websocket_serializers.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import unittest
|
||||
|
||||
from ..labeler import _Type
|
||||
|
||||
from ..websocket import serializers
|
||||
|
||||
class TestWebsocketSerializers(unittest.TestCase):
|
||||
def test_websocket_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 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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
7
bfxapi/urls.py
Normal file
7
bfxapi/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
REST_HOST = "https://api.bitfinex.com/v2"
|
||||
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
|
||||
STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2"
|
||||
|
||||
WSS_HOST = "wss://api.bitfinex.com/ws/2"
|
||||
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
|
||||
STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2"
|
||||
29
bfxapi/utils/JSONEncoder.py
Normal file
29
bfxapi/utils/JSONEncoder.py
Normal file
@@ -0,0 +1,29 @@
|
||||
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 != None}
|
||||
|
||||
def _convert_float_to_str(data: JSON) -> JSON:
|
||||
if isinstance(data, float):
|
||||
return format(Decimal(repr(data)), "f")
|
||||
elif isinstance(data, list):
|
||||
return [ _convert_float_to_str(sub_data) for sub_data in data ]
|
||||
elif isinstance(data, dict):
|
||||
return _strip({ key: _convert_float_to_str(value) for key, value in data.items() })
|
||||
else: return data
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
def encode(self, obj: JSON) -> str:
|
||||
return json.JSONEncoder.encode(self, _convert_float_to_str(obj))
|
||||
|
||||
def default(self, obj: Any) -> Any:
|
||||
if isinstance(obj, Decimal): return format(obj, "f")
|
||||
elif isinstance(obj, datetime): return str(obj)
|
||||
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
@@ -1 +1 @@
|
||||
NAME = 'utils'
|
||||
NAME = "utils"
|
||||
@@ -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
|
||||
22
bfxapi/utils/camel_and_snake_case_helpers.py
Normal file
22
bfxapi/utils/camel_and_snake_case_helpers.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import re
|
||||
|
||||
from typing import TypeVar, Callable, Dict, Any, cast
|
||||
|
||||
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 ])
|
||||
elif isinstance(data, dict):
|
||||
return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() })
|
||||
else: 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
52
bfxapi/utils/logger.py
Normal file
52
bfxapi/utils/logger.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
||||
|
||||
RESET_SEQ = "\033[0m"
|
||||
|
||||
COLOR_SEQ = "\033[1;%dm"
|
||||
ITALIC_COLOR_SEQ = "\033[3;%dm"
|
||||
UNDERLINE_COLOR_SEQ = "\033[4;%dm"
|
||||
|
||||
BOLD_SEQ = "\033[1m"
|
||||
|
||||
def formatter_message(message, use_color = True):
|
||||
if use_color:
|
||||
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
|
||||
else:
|
||||
message = message.replace("$RESET", "").replace("$BOLD", "")
|
||||
return message
|
||||
|
||||
COLORS = {
|
||||
"DEBUG": CYAN,
|
||||
"INFO": BLUE,
|
||||
"WARNING": YELLOW,
|
||||
"ERROR": RED
|
||||
}
|
||||
|
||||
class _ColoredFormatter(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:
|
||||
levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
|
||||
record.levelname = levelname_color
|
||||
record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ
|
||||
return logging.Formatter.format(self, record)
|
||||
|
||||
class ColoredLogger(logging.Logger):
|
||||
FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s"
|
||||
|
||||
COLOR_FORMAT = formatter_message(FORMAT, True)
|
||||
|
||||
def __init__(self, name, level):
|
||||
logging.Logger.__init__(self, name, level)
|
||||
|
||||
colored_formatter = _ColoredFormatter(self.COLOR_FORMAT)
|
||||
console = logging.StreamHandler()
|
||||
console.setFormatter(colored_formatter)
|
||||
|
||||
self.addHandler(console)
|
||||
@@ -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})
|
||||
@@ -1,5 +0,0 @@
|
||||
"""
|
||||
This module contains the current version of the bfxapi lib
|
||||
"""
|
||||
|
||||
__version__ = '2.0.6'
|
||||
3
bfxapi/websocket/__init__.py
Normal file
3
bfxapi/websocket/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs
|
||||
|
||||
NAME = "websocket"
|
||||
5
bfxapi/websocket/client/__init__.py
Normal file
5
bfxapi/websocket/client/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .bfx_websocket_client import BfxWebsocketClient
|
||||
from .bfx_websocket_bucket import BfxWebsocketBucket
|
||||
from .bfx_websocket_inputs import BfxWebsocketInputs
|
||||
|
||||
NAME = "client"
|
||||
107
bfxapi/websocket/client/bfx_websocket_bucket.py
Normal file
107
bfxapi/websocket/client/bfx_websocket_bucket.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import json, uuid, websockets
|
||||
|
||||
from typing import Literal, TypeVar, Callable, cast
|
||||
|
||||
from ..handlers import PublicChannelsHandler
|
||||
|
||||
from ..exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion
|
||||
|
||||
_HEARTBEAT = "hb"
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Literal[None]])
|
||||
|
||||
def _require_websocket_connection(function: F) -> F:
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
if self.websocket == None or self.websocket.open == False:
|
||||
raise ConnectionNotOpen("No open connection with the server.")
|
||||
|
||||
await function(self, *args, **kwargs)
|
||||
|
||||
return cast(F, wrapper)
|
||||
|
||||
class BfxWebsocketBucket(object):
|
||||
VERSION = 2
|
||||
|
||||
MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
|
||||
|
||||
def __init__(self, host, event_emitter, on_open_event):
|
||||
self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event
|
||||
|
||||
self.websocket, self.subscriptions, self.pendings = None, dict(), list()
|
||||
|
||||
self.handler = PublicChannelsHandler(event_emitter=self.event_emitter)
|
||||
|
||||
async def _connect(self, index):
|
||||
reconnection = False
|
||||
|
||||
async for websocket in websockets.connect(self.host):
|
||||
self.websocket = websocket
|
||||
|
||||
self.on_open_event.set()
|
||||
|
||||
if reconnection == True or (reconnection := False):
|
||||
for pending in self.pendings:
|
||||
await self.websocket.send(json.dumps(pending))
|
||||
|
||||
for _, subscription in self.subscriptions.items():
|
||||
await self._subscribe(**subscription)
|
||||
|
||||
self.subscriptions.clear()
|
||||
|
||||
try:
|
||||
async for message in websocket:
|
||||
message = json.loads(message)
|
||||
|
||||
if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]):
|
||||
self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ]
|
||||
self.subscriptions[chanId] = message
|
||||
self.event_emitter.emit("subscribed", message)
|
||||
elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]):
|
||||
if message["status"] == "OK":
|
||||
del self.subscriptions[chanId]
|
||||
elif isinstance(message, dict) and message["event"] == "error":
|
||||
self.event_emitter.emit("wss-error", message["code"], message["msg"])
|
||||
elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT:
|
||||
self.handler.handle(self.subscriptions[chanId], *message[1:])
|
||||
except websockets.ConnectionClosedError as error:
|
||||
if error.code == 1006:
|
||||
self.on_open_event.clear()
|
||||
reconnection = True
|
||||
continue
|
||||
|
||||
raise error
|
||||
|
||||
break
|
||||
|
||||
@_require_websocket_connection
|
||||
async def _subscribe(self, channel, subId=None, **kwargs):
|
||||
if len(self.subscriptions) + len(self.pendings) == BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT:
|
||||
raise TooManySubscriptions("The client has reached the maximum number of subscriptions.")
|
||||
|
||||
subscription = {
|
||||
**kwargs,
|
||||
|
||||
"event": "subscribe",
|
||||
"channel": channel,
|
||||
"subId": subId or str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
self.pendings.append(subscription)
|
||||
|
||||
await self.websocket.send(json.dumps(subscription))
|
||||
|
||||
@_require_websocket_connection
|
||||
async def _unsubscribe(self, chanId):
|
||||
await self.websocket.send(json.dumps({
|
||||
"event": "unsubscribe",
|
||||
"chanId": chanId
|
||||
}))
|
||||
|
||||
@_require_websocket_connection
|
||||
async def _close(self, code=1000, reason=str()):
|
||||
await self.websocket.close(code=code, reason=reason)
|
||||
|
||||
def _get_chan_id(self, subId):
|
||||
for subscription in self.subscriptions.values():
|
||||
if subscription["subId"] == subId:
|
||||
return subscription["chanId"]
|
||||
246
bfxapi/websocket/client/bfx_websocket_client.py
Normal file
246
bfxapi/websocket/client/bfx_websocket_client.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import traceback, json, asyncio, hmac, hashlib, time, websockets, socket, random
|
||||
|
||||
from typing import cast
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pyee.asyncio import AsyncIOEventEmitter
|
||||
|
||||
from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket
|
||||
|
||||
from .bfx_websocket_inputs import BfxWebsocketInputs
|
||||
from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler
|
||||
from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion
|
||||
|
||||
from ...utils.JSONEncoder import JSONEncoder
|
||||
|
||||
from ...utils.logger import ColoredLogger
|
||||
|
||||
def _require_websocket_authentication(function: F) -> F:
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
if hasattr(self, "authentication") and self.authentication == False:
|
||||
raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.")
|
||||
|
||||
await _require_websocket_connection(function)(self, *args, **kwargs)
|
||||
|
||||
return cast(F, wrapper)
|
||||
|
||||
class BfxWebsocketClient(object):
|
||||
VERSION = BfxWebsocketBucket.VERSION
|
||||
|
||||
MAXIMUM_CONNECTIONS_AMOUNT = 20
|
||||
|
||||
EVENTS = [
|
||||
"open", "subscribed", "authenticated", "wss-error",
|
||||
*PublicChannelsHandler.EVENTS,
|
||||
*AuthenticatedChannelsHandler.EVENTS
|
||||
]
|
||||
|
||||
def __init__(self, host, credentials = None, log_level = "INFO"):
|
||||
self.websocket = None
|
||||
|
||||
self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter()
|
||||
|
||||
self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input)
|
||||
|
||||
self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter)
|
||||
|
||||
self.logger = ColoredLogger("BfxWebsocketClient", level=log_level)
|
||||
|
||||
self.event_emitter.add_listener("error",
|
||||
lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" +
|
||||
str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1])
|
||||
)
|
||||
|
||||
def run(self, connections = 5):
|
||||
return asyncio.run(self.start(connections))
|
||||
|
||||
async def start(self, connections = 5):
|
||||
if connections > BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT:
|
||||
self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} buckets from the same " +
|
||||
f"connection ({connections} in use), the server could momentarily block the client with <429 Too Many Requests>.")
|
||||
|
||||
self.on_open_events = [ asyncio.Event() for _ in range(connections) ]
|
||||
|
||||
self.buckets = [
|
||||
BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index])
|
||||
for index in range(connections)
|
||||
]
|
||||
|
||||
tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ]
|
||||
|
||||
tasks.append(self.__connect(self.credentials))
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def __connect(self, credentials = None):
|
||||
Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"])
|
||||
|
||||
reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None
|
||||
|
||||
async def _connection():
|
||||
nonlocal reconnection
|
||||
|
||||
async with websockets.connect(self.host) as websocket:
|
||||
if reconnection.status == True:
|
||||
self.logger.info(f"Reconnect attempt successful (attempt no.{reconnection.attempts}): The " +
|
||||
f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " +
|
||||
f"(connection lost at: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).")
|
||||
|
||||
reconnection = Reconnection(status=False, attempts=0, timestamp=None)
|
||||
|
||||
self.websocket, self.authentication = websocket, False
|
||||
|
||||
if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]):
|
||||
self.event_emitter.emit("open")
|
||||
|
||||
if self.credentials:
|
||||
await self.__authenticate(**self.credentials)
|
||||
|
||||
async for message in websocket:
|
||||
message = json.loads(message)
|
||||
|
||||
if isinstance(message, dict) and message["event"] == "info" and "version" in message:
|
||||
if BfxWebsocketClient.VERSION != message["version"]:
|
||||
raise OutdatedClientVersion(f"Mismatch between the client version and the server version. " +
|
||||
f"Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, " +
|
||||
f"server version: {message['version']}).")
|
||||
elif isinstance(message, dict) and message["event"] == "info" and message["code"] == 20051:
|
||||
rcvd = websockets.frames.Close(code=1012, reason="Stop/Restart Websocket Server (please reconnect).")
|
||||
|
||||
raise websockets.ConnectionClosedError(rcvd=rcvd, sent=None)
|
||||
elif isinstance(message, dict) and message["event"] == "auth":
|
||||
if message["status"] == "OK":
|
||||
self.event_emitter.emit("authenticated", message); self.authentication = True
|
||||
else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.")
|
||||
elif isinstance(message, dict) and message["event"] == "error":
|
||||
self.event_emitter.emit("wss-error", message["code"], message["msg"])
|
||||
elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT:
|
||||
self.handler.handle(message[1], message[2])
|
||||
|
||||
class _Delay:
|
||||
BACKOFF_MIN, BACKOFF_MAX = 1.92, 60.0
|
||||
|
||||
BACKOFF_INITIAL = 5.0
|
||||
|
||||
def __init__(self, backoff_factor):
|
||||
self.__backoff_factor = backoff_factor
|
||||
self.__backoff_delay = _Delay.BACKOFF_MIN
|
||||
self.__initial_delay = random.random() * _Delay.BACKOFF_INITIAL
|
||||
|
||||
def next(self):
|
||||
backoff_delay = self.peek()
|
||||
__backoff_delay = self.__backoff_delay * self.__backoff_factor
|
||||
self.__backoff_delay = min(__backoff_delay, _Delay.BACKOFF_MAX)
|
||||
|
||||
return backoff_delay
|
||||
|
||||
def peek(self):
|
||||
return (self.__backoff_delay == _Delay.BACKOFF_MIN) \
|
||||
and self.__initial_delay or self.__backoff_delay
|
||||
|
||||
while True:
|
||||
if reconnection.status == True:
|
||||
await asyncio.sleep(delay.next())
|
||||
|
||||
try:
|
||||
await _connection()
|
||||
except (websockets.ConnectionClosedError, socket.gaierror) as error:
|
||||
if isinstance(error, websockets.ConnectionClosedError) and (error.code == 1006 or error.code == 1012):
|
||||
if error.code == 1006:
|
||||
self.logger.error("Connection lost: no close frame received "
|
||||
+ "or sent (1006). Attempting to reconnect...")
|
||||
|
||||
if error.code == 1012:
|
||||
self.logger.info("WSS server is about to restart, reconnection "
|
||||
+ "required (client received 20051). Attempt in progress...")
|
||||
|
||||
reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now());
|
||||
|
||||
delay = _Delay(backoff_factor=1.618)
|
||||
elif isinstance(error, socket.gaierror) and reconnection.status == True:
|
||||
self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. "
|
||||
+ f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds."
|
||||
+ f"(at the moment the client has been offline for {datetime.now() - reconnection.timestamp})")
|
||||
|
||||
reconnection = reconnection._replace(attempts=reconnection.attempts + 1)
|
||||
else: raise error
|
||||
|
||||
if reconnection.status == False:
|
||||
break
|
||||
|
||||
async def __authenticate(self, API_KEY, API_SECRET, filter=None):
|
||||
data = { "event": "auth", "filter": filter, "apiKey": API_KEY }
|
||||
|
||||
data["authNonce"] = int(round(time.time() * 1_000_000))
|
||||
|
||||
data["authPayload"] = "AUTH" + str(data["authNonce"])
|
||||
|
||||
data["authSig"] = hmac.new(
|
||||
API_SECRET.encode("utf8"),
|
||||
data["authPayload"].encode("utf8"),
|
||||
hashlib.sha384
|
||||
).hexdigest()
|
||||
|
||||
await self.websocket.send(json.dumps(data))
|
||||
|
||||
async def subscribe(self, channel, **kwargs):
|
||||
counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ]
|
||||
|
||||
index = counters.index(min(counters))
|
||||
|
||||
await self.buckets[index]._subscribe(channel, **kwargs)
|
||||
|
||||
async def unsubscribe(self, subId):
|
||||
for bucket in self.buckets:
|
||||
if (chanId := bucket._get_chan_id(subId)):
|
||||
await bucket._unsubscribe(chanId=chanId)
|
||||
|
||||
async def close(self, code=1000, reason=str()):
|
||||
if self.websocket != None and self.websocket.open == True:
|
||||
await self.websocket.close(code=code, reason=reason)
|
||||
|
||||
for bucket in self.buckets:
|
||||
await bucket._close(code=code, reason=reason)
|
||||
|
||||
@_require_websocket_authentication
|
||||
async def notify(self, info, MESSAGE_ID=None, **kwargs):
|
||||
await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ]))
|
||||
|
||||
@_require_websocket_authentication
|
||||
async def __handle_websocket_input(self, input, data):
|
||||
await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder))
|
||||
|
||||
def on(self, *events, callback = None):
|
||||
for event in events:
|
||||
if event not in BfxWebsocketClient.EVENTS:
|
||||
raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS")
|
||||
|
||||
if callback != None:
|
||||
for event in events:
|
||||
self.event_emitter.on(event, callback)
|
||||
|
||||
if callback == None:
|
||||
def handler(function):
|
||||
for event in events:
|
||||
self.event_emitter.on(event, function)
|
||||
|
||||
return handler
|
||||
|
||||
def once(self, *events, callback = None):
|
||||
for event in events:
|
||||
if event not in BfxWebsocketClient.EVENTS:
|
||||
raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS")
|
||||
|
||||
if callback != None:
|
||||
for event in events:
|
||||
self.event_emitter.once(event, callback)
|
||||
|
||||
if callback == None:
|
||||
def handler(function):
|
||||
for event in events:
|
||||
self.event_emitter.once(event, function)
|
||||
|
||||
return handler
|
||||
60
bfxapi/websocket/client/bfx_websocket_inputs.py
Normal file
60
bfxapi/websocket/client/bfx_websocket_inputs.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
|
||||
from typing import Union, Optional, List, Tuple
|
||||
from .. enums import OrderType, FundingOfferType
|
||||
from ... utils.JSONEncoder import JSON
|
||||
|
||||
class BfxWebsocketInputs(object):
|
||||
def __init__(self, handle_websocket_input):
|
||||
self.handle_websocket_input = handle_websocket_input
|
||||
|
||||
async 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):
|
||||
await self.handle_websocket_input("on", {
|
||||
"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
|
||||
})
|
||||
|
||||
async 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):
|
||||
await self.handle_websocket_input("ou", {
|
||||
"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
|
||||
})
|
||||
|
||||
async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None):
|
||||
await self.handle_websocket_input("oc", {
|
||||
"id": id, "cid": cid, "cid_date": cid_date
|
||||
})
|
||||
|
||||
async 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):
|
||||
await self.handle_websocket_input("oc_multi", {
|
||||
"ids": ids, "cids": cids, "gids": gids,
|
||||
"all": int(all)
|
||||
})
|
||||
|
||||
async 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):
|
||||
await self.handle_websocket_input("fon", {
|
||||
"type": type, "symbol": symbol, "amount": amount,
|
||||
"rate": rate, "period": period,
|
||||
"flags": flags
|
||||
})
|
||||
|
||||
async def cancel_funding_offer(self, id: int):
|
||||
await self.handle_websocket_input("foc", { "id": id })
|
||||
|
||||
async def calc(self, *args: str):
|
||||
await self.handle_websocket_input("calc", list(map(lambda arg: [arg], args)))
|
||||
8
bfxapi/websocket/enums.py
Normal file
8
bfxapi/websocket/enums.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .. enums import *
|
||||
|
||||
class Channel(str, Enum):
|
||||
TICKER = "ticker"
|
||||
TRADES = "trades"
|
||||
BOOK = "book"
|
||||
CANDLES = "candles"
|
||||
STATUS = "status"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user