Merge pull request #235 from Davi0kProgramsThings/fix/refactoring

Merge branch `Davi0kProgramsThings:fix/refactoring` into branch `bitfinexcom:master`.
This commit is contained in:
Dario Moceri
2024-03-05 15:38:02 +01:00
committed by GitHub
49 changed files with 2149 additions and 1611 deletions

13
.flake8 Normal file
View File

@@ -0,0 +1,13 @@
[flake8]
max-line-length = 80
extend-select = B950
extend-ignore = E203,E501,E701
exclude =
__pycache__
build
dist
venv
per-file-ignores =
*/__init__.py:F401

View File

@@ -20,9 +20,9 @@ A possible solution could be...
## Steps to reproduce (for bugs)
<!-- You can delete this section if you are not submitting a bug report -->
1. &nbsp;
2. &nbsp;
3. &nbsp;
1.
2.
3.
### Python version
<!-- Indicate your python version here -->

View File

@@ -20,10 +20,11 @@ PR fixes the following issue:
# Checklist:
- [ ] My code follows the style guidelines of this project;
- [ ] I have performed a self-review of my code;
- [ ] I have commented my code, particularly in hard-to-understand areas;
- [ ] I have made corresponding changes to the documentation;
- [ ] My changes generate no new warnings;
- [ ] Mypy returns no errors or warnings when run on the root package;
- [ ] Pylint returns a score of 10.00/10.00 when run on the root package;
- [ ] I've done a self-review of my code;
- [ ] I've made corresponding changes to the documentation;
- [ ] I've made sure my changes generate no warnings;
- [ ] mypy returns no errors when run on the root package;
<!-- If you use pre-commit hooks you can always check off the following tasks -->
- [ ] I've run black to format my code;
- [ ] I've run isort to format my code's import statements;
- [ ] flake8 reports no errors when run on the entire code base;

View File

@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -23,7 +20,7 @@ jobs:
python-version: '3.8'
- name: Install bitfinex-api-py's dependencies
run: python -m pip install -r dev-requirements.txt
- name: Lint the project with pylint (and fail if score is lower than 10.00/10.00)
run: python -m pylint bfxapi
- name: Run mypy to check the correctness of type hinting (and fail if any error or warning is found)
- name: Run pre-commit hooks (see .pre-commit-config.yaml)
uses: pre-commit/action@v3.0.1
- name: Run mypy to ensure correct type hinting
run: python -m mypy bfxapi

22
.gitignore vendored
View File

@@ -1,12 +1,16 @@
.venv
.DS_Store
.vscode
*.pyc
*.log
bitfinex_api_py.egg-info
.python-version
__pycache__
dist
venv
!.gitkeep
MANIFEST
bitfinex_api_py.egg-info
bitfinex_api_py.dist-info
build/
dist/
pip-wheel-metadata/
.eggs
.idea
venv/

2
.isort.cfg Normal file
View File

@@ -0,0 +1,2 @@
[settings]
profile = black

17
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,17 @@
repos:
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.2.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies: [
flake8-bugbear
]

View File

@@ -1,25 +0,0 @@
[MAIN]
py-version=3.8.0
[MESSAGES CONTROL]
disable=
missing-docstring,
multiple-imports,
too-few-public-methods,
too-many-instance-attributes
[VARIABLES]
allowed-redefined-builtins=all,dir,format,id,len,type
[FORMAT]
max-line-length=120
expected-line-ending-format=LF
[BASIC]
good-names=f,t,id,ip,on,pl,tf,to,A,B,C,D,E,F
[TYPECHECK]
generated-members=websockets
[STRING]
check-quote-consistency=yes

View File

@@ -312,7 +312,9 @@ Contributors must uphold the [Contributor Covenant code of conduct](https://gith
1. [Installation and setup](#installation-and-setup)
* [Cloning the repository](#cloning-the-repository)
* [Installing the dependencies](#installing-the-dependencies)
* [Set up the pre-commit hooks (optional)](#set-up-the-pre-commit-hooks-optional)
2. [Before opening a PR](#before-opening-a-pr)
* [Tip](#tip)
3. [License](#license)
## Installation and setup
@@ -333,23 +335,48 @@ git clone --branch v3-beta --single-branch https://github.com/bitfinexcom/bitfin
python3 -m pip install -r dev-requirements.txt
```
Make sure to install `dev-requirements.txt` instead of `requirements.txt`. \
`dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependencies. \
This will also install the versions in use of [`pylint`](https://github.com/pylint-dev/pylint) and [`mypy`](https://github.com/python/mypy), which you should both use before opening your PRs.
Make sure to install `dev-requirements.txt` (and not `requirements.txt`!). \
`dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependency. \
dev-requirements includes [mypy](https://github.com/python/mypy), [black](https://github.com/psf/black), [isort](https://github.com/PyCQA/isort), [flake8](https://github.com/PyCQA/flake8), and [pre-commit](https://github.com/pre-commit/pre-commit) (more on these tools in later chapters).
All done, your Python 3.8+ environment should now be able to run `bitfinex-api-py`'s source code.
### Set up the pre-commit hooks (optional)
**Do not skip this paragraph if you intend to contribute to the project.**
This repository includes a pre-commit configuration file that defines the following hooks:
1. [isort](https://github.com/PyCQA/isort)
2. [black](https://github.com/psf/black)
3. [flake8](https://github.com/PyCQA/flake8)
To set up pre-commit use:
```console
python3 -m pre-commit install
```
These will ensure that isort, black and flake8 are run on each git commit.
[Visit this page to learn more about git hooks and pre-commit.](https://pre-commit.com/#introduction)
#### Manually triggering the pre-commit hooks
You can also manually trigger the execution of all hooks with:
```console
python3 -m pre-commit run --all-files
```
## Before opening a PR
**We won't accept your PR or we will request changes if the following requirements aren't met.**
**We won't accept your PR or we'll request changes if the following requirements aren't met.**
Wheter you're submitting a bug fix, a new feature or a documentation change, you should first discuss it in an issue.
All PRs must follow this [PULL_REQUEST_TEMPLATE](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/.github/PULL_REQUEST_TEMPLATE.md) and include an exhaustive description.
You must be able to check off all tasks listed in [PULL_REQUEST_TEMPLATE](https://raw.githubusercontent.com/bitfinexcom/bitfinex-api-py/master/.github/PULL_REQUEST_TEMPLATE.md) before opening a pull request.
Before opening a pull request, you should also make sure that:
- [ ] [`pylint`](https://github.com/pylint-dev/pylint) returns a score of 10.00/10.00 when run against your code.
- [ ] [`mypy`](https://github.com/python/mypy) doesn't throw any error code when run on the project (excluding notes).
### Tip
Setting up the project's pre-commit hooks will help automate this process ([more](#set-up-the-pre-commit-hooks-optional)).
## License

View File

@@ -1,6 +1 @@
from ._client import \
Client, \
REST_HOST, \
WSS_HOST, \
PUB_REST_HOST, \
PUB_WSS_HOST
from ._client import PUB_REST_HOST, PUB_WSS_HOST, REST_HOST, WSS_HOST, Client

View File

@@ -1,15 +1,12 @@
from typing import \
TYPE_CHECKING, List, Optional
from typing import TYPE_CHECKING, List, Optional
from bfxapi._utils.logging import ColorLogger
from bfxapi.exceptions import IncompleteCredentialError
from bfxapi.rest import BfxRestInterface
from bfxapi.websocket import BfxWebSocketClient
from bfxapi.exceptions import IncompleteCredentialError
if TYPE_CHECKING:
from bfxapi.websocket._client.bfx_websocket_client import \
_Credentials
from bfxapi.websocket._client.bfx_websocket_client import _Credentials
REST_HOST = "https://api.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2"
@@ -17,29 +14,35 @@ WSS_HOST = "wss://api.bitfinex.com/ws/2"
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
class Client:
def __init__(
self,
api_key: Optional[str] = None,
api_secret: Optional[str] = None,
*,
rest_host: str = REST_HOST,
wss_host: str = WSS_HOST,
filters: Optional[List[str]] = None,
timeout: Optional[int] = 60 * 15,
log_filename: Optional[str] = None
self,
api_key: Optional[str] = None,
api_secret: Optional[str] = None,
*,
rest_host: str = REST_HOST,
wss_host: str = WSS_HOST,
filters: Optional[List[str]] = None,
timeout: Optional[int] = 60 * 15,
log_filename: Optional[str] = None,
) -> None:
credentials: Optional["_Credentials"] = None
if api_key and api_secret:
credentials = \
{ "api_key": api_key, "api_secret": api_secret, "filters": filters }
credentials = {
"api_key": api_key,
"api_secret": api_secret,
"filters": filters,
}
elif api_key:
raise IncompleteCredentialError( \
"You must provide both an API-KEY and an API-SECRET (missing API-KEY).")
raise IncompleteCredentialError(
"You must provide both API-KEY and API-SECRET (missing API-KEY)."
)
elif api_secret:
raise IncompleteCredentialError( \
"You must provide both an API-KEY and an API-SECRET (missing API-SECRET).")
raise IncompleteCredentialError(
"You must provide both API-KEY and API-SECRET (missing API-SECRET)."
)
self.rest = BfxRestInterface(rest_host, api_key, api_secret)
@@ -48,5 +51,6 @@ class Client:
if log_filename:
logger.register(filename=log_filename)
self.wss = BfxWebSocketClient(wss_host, \
credentials=credentials, timeout=timeout, logger=logger)
self.wss = BfxWebSocketClient(
wss_host, credentials=credentials, timeout=timeout, logger=logger
)

View File

@@ -1,13 +1,16 @@
from typing import Dict, Any
import json
import re
from typing import Any, Dict
import re, json
def _to_snake_case(string: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", string).lower()
def _object_hook(data: Dict[str, Any]) -> Any:
return { _to_snake_case(key): value for key, value in data.items() }
return {_to_snake_case(key): value for key, value in data.items()}
class JSONDecoder(json.JSONDecoder):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(object_hook=_object_hook, *args, **kwargs)
super().__init__(*args, **kwargs, object_hook=_object_hook)

View File

@@ -1,20 +1,17 @@
from typing import \
Union, List, Dict, \
Any
import json
from decimal import Decimal
from typing import Any, Dict, List, Union
_ExtJSON = Union[Dict[str, "_ExtJSON"], List["_ExtJSON"], \
bool, int, float, str, Decimal, None]
_ExtJSON = Union[
Dict[str, "_ExtJSON"], List["_ExtJSON"], bool, int, float, str, Decimal, None
]
_StrictJSON = Union[Dict[str, "_StrictJSON"], List["_StrictJSON"], int, str, None]
_StrictJSON = Union[Dict[str, "_StrictJSON"], List["_StrictJSON"], \
int, str, None]
def _clear(dictionary: Dict[str, Any]) -> Dict[str, Any]:
return { key: value for key, value in dictionary.items() \
if value is not None }
return {key: value for key, value in dictionary.items() if value is not None}
def _adapter(data: _ExtJSON) -> _StrictJSON:
if isinstance(data, bool):
@@ -25,12 +22,13 @@ def _adapter(data: _ExtJSON) -> _StrictJSON:
return format(data, "f")
if isinstance(data, list):
return [ _adapter(sub_data) for sub_data in data ]
return [_adapter(sub_data) for sub_data in data]
if isinstance(data, dict):
return _clear({ key: _adapter(value) for key, value in data.items() })
return _clear({key: _adapter(value) for key, value in data.items()})
return data
class JSONEncoder(json.JSONEncoder):
def encode(self, o: _ExtJSON) -> str:
return super().encode(_adapter(o))

View File

@@ -1,33 +1,36 @@
from typing import \
TYPE_CHECKING, Literal, Optional
#pylint: disable-next=wildcard-import,unused-wildcard-import
from logging import *
from copy import copy
import sys
from copy import copy
from logging import FileHandler, Formatter, Logger, LogRecord, StreamHandler
from typing import TYPE_CHECKING, Literal, Optional
if TYPE_CHECKING:
_Level = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
_BLACK, _RED, _GREEN, _YELLOW, \
_BLUE, _MAGENTA, _CYAN, _WHITE = \
[ f"\033[0;{90 + i}m" for i in range(8) ]
_BLACK, _RED, _GREEN, _YELLOW, _BLUE, _MAGENTA, _CYAN, _WHITE = [
f"\033[0;{90 + i}m" for i in range(8)
]
_BOLD_BLACK, _BOLD_RED, _BOLD_GREEN, _BOLD_YELLOW, \
_BOLD_BLUE, _BOLD_MAGENTA, _BOLD_CYAN, _BOLD_WHITE = \
[ f"\033[1;{90 + i}m" for i in range(8) ]
(
_BOLD_BLACK,
_BOLD_RED,
_BOLD_GREEN,
_BOLD_YELLOW,
_BOLD_BLUE,
_BOLD_MAGENTA,
_BOLD_CYAN,
_BOLD_WHITE,
) = [f"\033[1;{90 + i}m" for i in range(8)]
_NC = "\033[0m"
class _ColorFormatter(Formatter):
__LEVELS = {
"INFO": _BLUE,
"WARNING": _YELLOW,
"ERROR": _RED,
"CRITICAL": _BOLD_RED,
"DEBUG": _BOLD_WHITE
"DEBUG": _BOLD_WHITE,
}
def format(self, record: LogRecord) -> str:
@@ -37,7 +40,6 @@ class _ColorFormatter(Formatter):
return super().format(_record)
#pylint: disable-next=invalid-name
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
return _GREEN + super().formatTime(record, datefmt) + _NC
@@ -45,12 +47,14 @@ class _ColorFormatter(Formatter):
def __format_level(level: str) -> str:
return _ColorFormatter.__LEVELS[level] + level + _NC
_FORMAT = "%(asctime)s %(name)s %(levelname)s %(message)s"
_DATE_FORMAT = "%d-%m-%Y %H:%M:%S"
class ColorLogger(Logger):
__FORMATTER = Formatter(_FORMAT,_DATE_FORMAT)
__FORMATTER = Formatter(_FORMAT, _DATE_FORMAT)
def __init__(self, name: str, level: "_Level" = "NOTSET") -> None:
super().__init__(name, level)

View File

@@ -3,8 +3,10 @@ class BfxBaseException(Exception):
Base class for every custom exception thrown by bitfinex-api-py.
"""
class IncompleteCredentialError(BfxBaseException):
pass
class InvalidCredentialError(BfxBaseException):
pass

View File

@@ -1,2 +1,6 @@
from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthEndpoints, \
RestMerchantEndpoints
from .endpoints import (
BfxRestInterface,
RestAuthEndpoints,
RestMerchantEndpoints,
RestPublicEndpoints,
)

View File

@@ -1,5 +1,4 @@
from .bfx_rest_interface import BfxRestInterface
from .rest_public_endpoints import RestPublicEndpoints
from .rest_auth_endpoints import RestAuthEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints
from .rest_public_endpoints import RestPublicEndpoints

View File

@@ -1,11 +1,14 @@
from .rest_public_endpoints import RestPublicEndpoints
from .rest_auth_endpoints import RestAuthEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints
from .rest_public_endpoints import RestPublicEndpoints
class BfxRestInterface:
VERSION = 2
def __init__(self, host, api_key = None, api_secret = None):
def __init__(self, host, api_key=None, api_secret=None):
self.public = RestPublicEndpoints(host=host)
self.auth = RestAuthEndpoints(host=host, api_key=api_key, api_secret=api_secret)
self.merchant = RestMerchantEndpoints(host=host, api_key=api_key, api_secret=api_secret)
self.merchant = RestMerchantEndpoints(
host=host, api_key=api_key, api_secret=api_secret
)

View File

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

View File

@@ -1,113 +1,140 @@
from typing import \
TypedDict, Dict, List, \
Union, Literal, Optional, \
Any
from decimal import Decimal
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
from bfxapi.rest.middleware import Middleware
from bfxapi.types import (
CurrencyConversion,
InvoicePage,
InvoiceStats,
InvoiceSubmission,
MerchantDeposit,
MerchantUnlinkedDeposit,
)
from bfxapi.types import \
InvoiceSubmission, \
InvoicePage, \
InvoiceStats, \
CurrencyConversion, \
MerchantDeposit, \
MerchantUnlinkedDeposit
_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,
},
)
_CustomerInfo = TypedDict("_CustomerInfo", {
"nationality": str,
"resid_country": str,
"resid_city": str,
"resid_zip_code": str,
"resid_street": str,
"resid_building_no": str,
"full_name": str,
"email": str,
"tos_accepted": bool
})
class RestMerchantEndpoints(Middleware):
#pylint: disable-next=too-many-arguments
def submit_invoice(self,
amount: Union[str, float, Decimal],
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:
def submit_invoice(
self,
amount: Union[str, float, Decimal],
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 = {
"amount": amount, "currency": currency, "orderId": order_id,
"customerInfo": customer_info, "payCurrencies": pay_currencies, "duration": duration,
"webhook": webhook, "redirectUrl": redirect_url
"amount": amount,
"currency": currency,
"orderId": order_id,
"customerInfo": customer_info,
"payCurrencies": pay_currencies,
"duration": duration,
"webhook": webhook,
"redirectUrl": redirect_url,
}
data = self._post("auth/w/ext/pay/invoice/create", body=body)
return InvoiceSubmission.parse(data)
def get_invoices(self,
*,
id: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[InvoiceSubmission]:
body = {
"id": id, "start": start, "end": end,
"limit": limit
}
def get_invoices(
self,
*,
id: Optional[str] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[InvoiceSubmission]:
body = {"id": id, "start": start, "end": end, "limit": limit}
data = self._post("auth/r/ext/pay/invoices", body=body)
return [ InvoiceSubmission.parse(sub_data) for sub_data in data ]
return [InvoiceSubmission.parse(sub_data) for sub_data in data]
def get_invoices_paginated(self,
page: int = 1,
page_size: int = 10,
sort: Literal["asc", "desc"] = "asc",
sort_field: Literal["t", "amount", "status"] = "t",
*,
status: Optional[List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]] = None,
fiat: Optional[List[str]] = None,
crypto: Optional[List[str]] = None,
id: Optional[str] = None,
order_id: Optional[str] = None) -> InvoicePage:
def get_invoices_paginated(
self,
page: int = 1,
page_size: int = 10,
sort: Literal["asc", "desc"] = "asc",
sort_field: Literal["t", "amount", "status"] = "t",
*,
status: Optional[
List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]
] = None,
fiat: Optional[List[str]] = None,
crypto: Optional[List[str]] = None,
id: Optional[str] = None,
order_id: Optional[str] = None,
) -> InvoicePage:
body = {
"page": page, "pageSize": page_size, "sort": sort,
"sortField": sort_field, "status": status, "fiat": fiat,
"crypto": crypto, "id": id, "orderId": order_id
"page": page,
"pageSize": page_size,
"sort": sort,
"sortField": sort_field,
"status": status,
"fiat": fiat,
"crypto": crypto,
"id": id,
"orderId": order_id,
}
data = self._post("auth/r/ext/pay/invoices/paginated", body=body)
return InvoicePage.parse(data)
def get_invoice_count_stats(self,
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"],
format: str) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in \
self._post("auth/r/ext/pay/invoice/stats/count", \
body={ "status": status, "format": format }) ]
def get_invoice_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 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:
def complete_invoice(
self,
id: str,
pay_currency: str,
*,
deposit_id: Optional[int] = None,
ledger_id: Optional[int] = None,
) -> InvoiceSubmission:
body = {
"id": id, "payCcy": pay_currency, "depositId": deposit_id,
"ledgerId": ledger_id
"id": id,
"payCcy": pay_currency,
"depositId": deposit_id,
"ledgerId": ledger_id,
}
data = self._post("auth/w/ext/pay/invoice/complete", body=body)
@@ -115,65 +142,66 @@ class RestMerchantEndpoints(Middleware):
return InvoiceSubmission.parse(data)
def expire_invoice(self, id: str) -> InvoiceSubmission:
body = { "id": id }
body = {"id": id}
data = self._post("auth/w/ext/pay/invoice/expire", body=body)
return InvoiceSubmission.parse(data)
def get_currency_conversion_list(self) -> List[CurrencyConversion]:
return [ CurrencyConversion(**sub_data) \
for sub_data in self._post("auth/r/ext/pay/settings/convert/list") ]
return [
CurrencyConversion(**sub_data)
for sub_data in self._post("auth/r/ext/pay/settings/convert/list")
]
def add_currency_conversion(self,
base_ccy: str,
convert_ccy: str) -> bool:
return bool(self._post("auth/w/ext/pay/settings/convert/create", \
body={ "baseCcy": base_ccy, "convertCcy": convert_ccy }))
def add_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool:
return bool(
self._post(
"auth/w/ext/pay/settings/convert/create",
body={"baseCcy": base_ccy, "convertCcy": convert_ccy},
)
)
def remove_currency_conversion(self,
base_ccy: str,
convert_ccy: str) -> bool:
return bool(self._post("auth/w/ext/pay/settings/convert/remove", \
body={ "baseCcy": base_ccy, "convertCcy": convert_ccy }))
def remove_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool:
return bool(
self._post(
"auth/w/ext/pay/settings/convert/remove",
body={"baseCcy": base_ccy, "convertCcy": convert_ccy},
)
)
def set_merchant_settings(self,
key: str,
val: Any) -> bool:
return bool(self._post("auth/w/ext/pay/settings/set", \
body={ "key": key, "val": val }))
def set_merchant_settings(self, key: str, val: Any) -> bool:
return bool(
self._post("auth/w/ext/pay/settings/set", body={"key": key, "val": val})
)
def get_merchant_settings(self, key: str) -> Any:
return self._post("auth/r/ext/pay/settings/get", body={ "key": key })
return self._post("auth/r/ext/pay/settings/get", body={"key": key})
#pylint: disable-next=dangerous-default-value
def list_merchant_settings(self, keys: List[str] = []) -> Dict[str, Any]:
return self._post("auth/r/ext/pay/settings/list", body={ "keys": keys })
def list_merchant_settings(
self, keys: Optional[List[str]] = None
) -> Dict[str, Any]:
return self._post("auth/r/ext/pay/settings/list", body={"keys": keys or []})
def get_deposits(self,
start: int,
to: int,
*,
ccy: Optional[str] = None,
unlinked: Optional[bool] = None) -> List[MerchantDeposit]:
body = {
"from": start, "to": to, "ccy": ccy,
"unlinked": unlinked
}
def get_deposits(
self,
start: int,
to: int,
*,
ccy: Optional[str] = None,
unlinked: Optional[bool] = None,
) -> List[MerchantDeposit]:
body = {"from": start, "to": to, "ccy": ccy, "unlinked": unlinked}
data = self._post("auth/r/ext/pay/deposits", body=body)
return [ MerchantDeposit(**sub_data) for sub_data in data ]
return [MerchantDeposit(**sub_data) for sub_data in data]
def get_unlinked_deposits(self,
ccy: str,
*,
start: Optional[int] = None,
end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]:
body = {
"ccy": ccy, "start": start, "end": end
}
def get_unlinked_deposits(
self, ccy: str, *, start: Optional[int] = None, end: Optional[int] = None
) -> List[MerchantUnlinkedDeposit]:
body = {"ccy": ccy, "start": start, "end": end}
data = self._post("/auth/r/ext/pay/deposits/unlinked", body=body)
return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in data ]
return [MerchantUnlinkedDeposit(**sub_data) for sub_data in data]

View File

@@ -1,21 +1,33 @@
from typing import List, Dict, Union, Literal, Optional, Any, cast
from decimal import Decimal
from typing import Any, Dict, List, Literal, Optional, Union, cast
from ...types import (
Candle,
DerivativesStatus,
FundingCurrencyBook,
FundingCurrencyRawBook,
FundingCurrencyTicker,
FundingCurrencyTrade,
FundingMarketAveragePrice,
FundingStatistic,
FxRate,
Leaderboard,
Liquidation,
PlatformStatus,
PulseMessage,
PulseProfile,
Statistic,
TickersHistory,
TradingMarketAveragePrice,
TradingPairBook,
TradingPairRawBook,
TradingPairTicker,
TradingPairTrade,
serializers,
)
from ..middleware import Middleware
from ...types import \
PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \
TickersHistory, TradingPairTrade, FundingCurrencyTrade, \
TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \
FundingCurrencyRawBook, Statistic, Candle, \
DerivativesStatus, Liquidation, Leaderboard, \
FundingStatistic, PulseProfile, PulseMessage, \
TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate
from ...types import serializers
#pylint: disable-next=too-many-public-methods
class RestPublicEndpoints(Middleware):
def conf(self, config: str) -> Any:
return self._get(f"conf/{config}")[0]
@@ -23,35 +35,47 @@ class RestPublicEndpoints(Middleware):
def get_platform_status(self) -> PlatformStatus:
return serializers.PlatformStatus.parse(*self._get("platform/status"))
def get_tickers(self, symbols: List[str]) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]:
data = self._get("tickers", params={ "symbols": ",".join(symbols) })
def get_tickers(
self, symbols: List[str]
) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]:
data = self._get("tickers", params={"symbols": ",".join(symbols)})
parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }
return {
symbol: cast(Union[TradingPairTicker, FundingCurrencyTicker],
parsers[symbol[0]](*sub_data)) for sub_data in data
if (symbol := sub_data.pop(0))
parsers = {
"t": serializers.TradingPairTicker.parse,
"f": serializers.FundingCurrencyTicker.parse,
}
def get_t_tickers(self, symbols: Union[List[str], Literal["ALL"]]) -> Dict[str, TradingPairTicker]:
return {
symbol: cast(
Union[TradingPairTicker, FundingCurrencyTicker],
parsers[symbol[0]](*sub_data),
)
for sub_data in data
if (symbol := sub_data.pop(0))
}
def get_t_tickers(
self, symbols: Union[List[str], Literal["ALL"]]
) -> Dict[str, TradingPairTicker]:
if isinstance(symbols, str) and symbols == "ALL":
return {
symbol: cast(TradingPairTicker, sub_data)
for symbol, sub_data in self.get_tickers([ "ALL" ]).items()
if symbol.startswith("t")
for symbol, sub_data in self.get_tickers(["ALL"]).items()
if symbol.startswith("t")
}
data = self.get_tickers(list(symbols))
return cast(Dict[str, TradingPairTicker], data)
def get_f_tickers(self, symbols: Union[List[str], Literal["ALL"]]) -> Dict[str, FundingCurrencyTicker]:
def get_f_tickers(
self, symbols: Union[List[str], Literal["ALL"]]
) -> Dict[str, FundingCurrencyTicker]:
if isinstance(symbols, str) and symbols == "ALL":
return {
symbol: cast(FundingCurrencyTicker, sub_data)
for symbol, sub_data in self.get_tickers([ "ALL" ]).items()
if symbol.startswith("f")
for symbol, sub_data in self.get_tickers(["ALL"]).items()
if symbol.startswith("f")
}
data = self.get_tickers(list(symbols))
@@ -64,230 +88,292 @@ class RestPublicEndpoints(Middleware):
def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker:
return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{symbol}"))
def get_tickers_history(self,
symbols: List[str],
*,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[TickersHistory]:
return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._get("tickers/hist", params={
"symbols": ",".join(symbols),
"start": start, "end": end,
"limit": limit
}) ]
def get_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[int] = None) -> List[TradingPairTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
def get_t_trades(
self,
pair: str,
*,
limit: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
sort: Optional[int] = 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 ]
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[int] = None) -> List[FundingCurrencyTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
def get_f_trades(
self,
currency: str,
*,
limit: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
sort: Optional[int] = 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 ]
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_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_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_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_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[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Statistic]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_stats_hist(
self,
resource: str,
*,
sort: Optional[int] = 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 ]
return [serializers.Statistic.parse(*sub_data) for sub_data in data]
def get_stats_last(self,
resource: str,
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> Statistic:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_stats_last(
self,
resource: str,
*,
sort: Optional[int] = 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[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Candle]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_candles_hist(
self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[int] = 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 ]
return [serializers.Candle.parse(*sub_data) for sub_data in data]
def get_candles_last(self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> Candle:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_candles_last(
self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None,
) -> Candle:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"candles/trade:{tf}:{symbol}/last", params=params)
return serializers.Candle.parse(*data)
def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> Dict[str, DerivativesStatus]:
def get_derivatives_status(
self, keys: Union[List[str], Literal["ALL"]]
) -> Dict[str, DerivativesStatus]:
if keys == "ALL":
params = { "keys": "ALL" }
else: params = { "keys": ",".join(keys) }
params = {"keys": "ALL"}
else:
params = {"keys": ",".join(keys)}
data = self._get("status/deriv", params=params)
return {
key: serializers.DerivativesStatus.parse(*sub_data)
for sub_data in data
if (key := sub_data.pop(0))
for sub_data in data
if (key := sub_data.pop(0))
}
def get_derivatives_status_history(self,
key: str,
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[DerivativesStatus]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_derivatives_status_history(
self,
key: str,
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[DerivativesStatus]:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"status/deriv/{key}/hist", params=params)
return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ]
return [serializers.DerivativesStatus.parse(*sub_data) for sub_data in data]
def get_liquidations(self,
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Liquidation]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_liquidations(
self,
*,
sort: Optional[int] = 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 ]
return [serializers.Liquidation.parse(*sub_data[0]) for sub_data in data]
def get_seed_candles(self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Candle]:
def get_seed_candles(
self,
symbol: str,
tf: str = "1m",
*,
sort: Optional[int] = 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 ]
return [serializers.Candle.parse(*sub_data) for sub_data in data]
def get_leaderboards_hist(self,
resource: str,
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[Leaderboard]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_leaderboards_hist(
self,
resource: str,
*,
sort: Optional[int] = 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 ]
return [serializers.Leaderboard.parse(*sub_data) for sub_data in data]
def get_leaderboards_last(self,
resource: str,
*,
sort: Optional[int] = None,
start: Optional[str] = None,
end: Optional[str] = None,
limit: Optional[int] = None) -> Leaderboard:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
def get_leaderboards_last(
self,
resource: str,
*,
sort: Optional[int] = 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 }
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 ]
return [serializers.FundingStatistic.parse(*sub_data) for sub_data in data]
def get_pulse_profile_details(self, nickname: str) -> PulseProfile:
return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}"))
def get_pulse_message_history(self,
*,
end: Optional[str] = None,
limit: Optional[int] = None) -> List[PulseMessage]:
def get_pulse_message_history(
self, *, end: Optional[str] = None, limit: Optional[int] = None
) -> List[PulseMessage]:
messages = []
for sub_data in self._get("pulse/hist", params={ "end": end, "limit": limit }):
for sub_data in self._get("pulse/hist", params={"end": end, "limit": limit}):
sub_data[18] = sub_data[18][0]
message = serializers.PulseMessage.parse(*sub_data)
messages.append(message)
return messages
def get_trading_market_average_price(self,
symbol: str,
amount: Union[str, float, Decimal],
*,
price_limit: Optional[Union[str, float, Decimal]] = None
) -> TradingMarketAveragePrice:
return serializers.TradingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={
"symbol": symbol, "amount": amount, "price_limit": price_limit
}))
def get_trading_market_average_price(
self,
symbol: str,
amount: Union[str, float, Decimal],
*,
price_limit: Optional[Union[str, float, Decimal]] = 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[str, float, Decimal],
period: int,
*,
rate_limit: Optional[Union[str, float, Decimal]] = None
) -> FundingMarketAveragePrice:
return serializers.FundingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={
"symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit
}))
def get_funding_market_average_price(
self,
symbol: str,
amount: Union[str, float, Decimal],
period: int,
*,
rate_limit: Optional[Union[str, float, Decimal]] = 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 }))
return serializers.FxRate.parse(
*self._post("calc/fx", body={"ccy1": ccy1, "ccy2": ccy2})
)

View File

@@ -1,10 +1,13 @@
from bfxapi.exceptions import BfxBaseException
class NotFoundError(BfxBaseException):
pass
class RequestParametersError(BfxBaseException):
pass
class UnknownGenericError(BfxBaseException):
pass

View File

@@ -1,59 +1,62 @@
from typing import TYPE_CHECKING, Optional, Any
import hashlib
import hmac
import json
import time
from enum import IntEnum
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Optional
import time, hmac, hashlib, json, requests
import requests
from ..exceptions import NotFoundError, RequestParametersError, UnknownGenericError
from ...exceptions import InvalidCredentialError
from ..._utils.json_encoder import JSONEncoder
from ..._utils.json_decoder import JSONDecoder
from ..._utils.json_encoder import JSONEncoder
from ...exceptions import InvalidCredentialError
from ..exceptions import NotFoundError, RequestParametersError, UnknownGenericError
if TYPE_CHECKING:
from requests.sessions import _Params
class _Error(IntEnum):
ERR_UNK = 10000
ERR_GENERIC = 10001
ERR_PARAMS = 10020
ERR_AUTH_FAIL = 10100
class Middleware:
TIMEOUT = 30
def __init__(self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None):
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"
assert isinstance(self.api_key, str) and isinstance(
self.api_secret, str
), "API_KEY and API_SECRET must be both strings"
nonce = str(round(time.time() * 1_000_000))
if data is None:
path = f"/api/v2/{endpoint}{nonce}"
else: path = f"/api/v2/{endpoint}{nonce}{data}"
else:
path = f"/api/v2/{endpoint}{nonce}{data}"
signature = hmac.new(
self.api_secret.encode("utf8"),
path.encode("utf8"),
hashlib.sha384
self.api_secret.encode("utf8"), path.encode("utf8"), hashlib.sha384
).hexdigest()
return {
"bfx-nonce": nonce,
"bfx-signature": signature,
"bfx-apikey": self.api_key
"bfx-apikey": self.api_key,
}
def _get(self, endpoint: str, params: Optional["_Params"] = None) -> Any:
response = requests.get(
url=f"{self.host}/{endpoint}",
params=params,
timeout=Middleware.TIMEOUT
url=f"{self.host}/{endpoint}", params=params, timeout=Middleware.TIMEOUT
)
if response.status_code == HTTPStatus.NOT_FOUND:
@@ -63,30 +66,43 @@ class Middleware:
if len(data) and data[0] == "error":
if data[1] == _Error.ERR_PARAMS:
raise RequestParametersError("The request was rejected with the " \
f"following parameter error: <{data[2]}>")
raise RequestParametersError(
"The request was rejected with the "
f"following parameter error: <{data[2]}>"
)
if data[1] is None or data[1] == _Error.ERR_UNK or data[1] == _Error.ERR_GENERIC:
raise UnknownGenericError("The server replied to the request with " \
f"a generic error with message: <{data[2]}>.")
if (
data[1] is None
or data[1] == _Error.ERR_UNK
or data[1] == _Error.ERR_GENERIC
):
raise UnknownGenericError(
"The server replied to the request with "
f"a generic error with message: <{data[2]}>."
)
return data
def _post(self, endpoint: str, params: Optional["_Params"] = None,
body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any:
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" }
headers = {"Content-Type": "application/json"}
if self.api_key and self.api_secret and not _ignore_authentication_headers:
headers = { **headers, **self.__build_authentication_headers(endpoint, data) }
headers = {**headers, **self.__build_authentication_headers(endpoint, data)}
response = requests.post(
url=f"{self.host}/{endpoint}",
params=params,
data=data,
headers=headers,
timeout=Middleware.TIMEOUT
timeout=Middleware.TIMEOUT,
)
if response.status_code == HTTPStatus.NOT_FOUND:
@@ -96,14 +112,24 @@ class Middleware:
if isinstance(data, list) and len(data) and data[0] == "error":
if data[1] == _Error.ERR_PARAMS:
raise RequestParametersError("The request was rejected with the " \
f"following parameter error: <{data[2]}>")
raise RequestParametersError(
"The request was rejected with the "
f"following parameter error: <{data[2]}>"
)
if data[1] == _Error.ERR_AUTH_FAIL:
raise InvalidCredentialError("Cannot authenticate with given API-KEY and API-SECRET.")
raise InvalidCredentialError(
"Cannot authenticate with given API-KEY and API-SECRET."
)
if data[1] is None or data[1] == _Error.ERR_UNK or data[1] == _Error.ERR_GENERIC:
raise UnknownGenericError("The server replied to the request with " \
f"a generic error with message: <{data[2]}>.")
if (
data[1] is None
or data[1] == _Error.ERR_UNK
or data[1] == _Error.ERR_GENERIC
):
raise UnknownGenericError(
"The server replied to the request with "
f"a generic error with message: <{data[2]}>."
)
return data

View File

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

View File

@@ -1,16 +1,16 @@
from typing import \
List, Dict, Literal, Optional, Any
from dataclasses import dataclass
from typing import Any, Dict, List, Literal, Optional
from .labeler import _Type, partial, compose
from .labeler import _Type, compose, partial
# region Dataclass definitions for types of public use
#region Dataclass definitions for types of public use
@dataclass
class PlatformStatus(_Type):
status: int
@dataclass
class TradingPairTicker(_Type):
bid: float
@@ -24,6 +24,7 @@ class TradingPairTicker(_Type):
high: float
low: float
@dataclass
class FundingCurrencyTicker(_Type):
frr: float
@@ -41,6 +42,7 @@ class FundingCurrencyTicker(_Type):
low: float
frr_amount_available: float
@dataclass
class TickersHistory(_Type):
symbol: str
@@ -48,6 +50,7 @@ class TickersHistory(_Type):
ask: float
mts: int
@dataclass
class TradingPairTrade(_Type):
id: int
@@ -55,6 +58,7 @@ class TradingPairTrade(_Type):
amount: float
price: float
@dataclass
class FundingCurrencyTrade(_Type):
id: int
@@ -63,12 +67,14 @@ class FundingCurrencyTrade(_Type):
rate: float
period: int
@dataclass
class TradingPairBook(_Type):
price: float
count: int
amount: float
@dataclass
class FundingCurrencyBook(_Type):
rate: float
@@ -76,12 +82,14 @@ class FundingCurrencyBook(_Type):
count: int
amount: float
@dataclass
class TradingPairRawBook(_Type):
order_id: int
price: float
amount: float
@dataclass
class FundingCurrencyRawBook(_Type):
offer_id: int
@@ -89,11 +97,13 @@ class FundingCurrencyRawBook(_Type):
rate: float
amount: float
@dataclass
class Statistic(_Type):
mts: int
value: float
@dataclass
class Candle(_Type):
mts: int
@@ -103,6 +113,7 @@ class Candle(_Type):
low: int
volume: float
@dataclass
class DerivativesStatus(_Type):
mts: int
@@ -118,6 +129,7 @@ class DerivativesStatus(_Type):
clamp_min: float
clamp_max: float
@dataclass
class Liquidation(_Type):
pos_id: int
@@ -129,6 +141,7 @@ class Liquidation(_Type):
is_market_sold: int
liquidation_price: float
@dataclass
class Leaderboard(_Type):
mts: int
@@ -137,6 +150,7 @@ class Leaderboard(_Type):
value: float
twitter_handle: Optional[str]
@dataclass
class FundingStatistic(_Type):
mts: int
@@ -146,6 +160,7 @@ class FundingStatistic(_Type):
funding_amount_used: float
funding_below_threshold: float
@dataclass
class PulseProfile(_Type):
puid: str
@@ -158,6 +173,7 @@ class PulseProfile(_Type):
following: int
tipping_status: int
@dataclass
class PulseMessage(_Type):
pid: str
@@ -175,23 +191,28 @@ class PulseMessage(_Type):
profile: PulseProfile
comments: int
@dataclass
class TradingMarketAveragePrice(_Type):
price_avg: float
amount: float
@dataclass
class FundingMarketAveragePrice(_Type):
rate_avg: float
amount: float
@dataclass
class FxRate(_Type):
current_rate: float
#endregion
#region Dataclass definitions for types of auth use
# endregion
# region Dataclass definitions for types of auth use
@dataclass
class UserInfo(_Type):
@@ -224,6 +245,7 @@ class UserInfo(_Type):
compl_countries_resid: List[str]
is_merchant_enterprise: int
@dataclass
class LoginHistory(_Type):
id: int
@@ -231,10 +253,12 @@ class LoginHistory(_Type):
ip: str
extra_info: Dict[str, Any]
@dataclass
class BalanceAvailable(_Type):
amount: float
@dataclass
class Order(_Type):
id: int
@@ -260,6 +284,7 @@ class Order(_Type):
routing: str
meta: Dict[str, Any]
@dataclass
class Position(_Type):
symbol: str
@@ -280,6 +305,7 @@ class Position(_Type):
collateral_min: float
meta: Dict[str, Any]
@dataclass
class Trade(_Type):
id: int
@@ -290,11 +316,12 @@ class Trade(_Type):
exec_price: float
order_type: str
order_price: float
maker:int
maker: int
fee: float
fee_currency: str
cid: int
@dataclass()
class FundingTrade(_Type):
id: int
@@ -305,6 +332,7 @@ class FundingTrade(_Type):
rate: float
period: int
@dataclass
class OrderTrade(_Type):
id: int
@@ -313,11 +341,12 @@ class OrderTrade(_Type):
order_id: int
exec_amount: float
exec_price: float
maker:int
maker: int
fee: float
fee_currency: str
cid: int
@dataclass
class Ledger(_Type):
id: int
@@ -327,6 +356,7 @@ class Ledger(_Type):
balance: float
description: str
@dataclass
class FundingOffer(_Type):
id: int
@@ -344,6 +374,7 @@ class FundingOffer(_Type):
hidden: int
renew: int
@dataclass
class FundingCredit(_Type):
id: int
@@ -365,6 +396,7 @@ class FundingCredit(_Type):
no_close: int
position_pair: str
@dataclass
class FundingLoan(_Type):
id: int
@@ -385,6 +417,7 @@ class FundingLoan(_Type):
renew: int
no_close: int
@dataclass
class FundingAutoRenew(_Type):
currency: str
@@ -392,6 +425,7 @@ class FundingAutoRenew(_Type):
rate: float
threshold: float
@dataclass()
class FundingInfo(_Type):
yield_loan: float
@@ -399,6 +433,7 @@ class FundingInfo(_Type):
duration_loan: float
duration_lend: float
@dataclass
class Wallet(_Type):
wallet_type: str
@@ -409,6 +444,7 @@ class Wallet(_Type):
last_change: str
trade_details: Dict[str, Any]
@dataclass
class Transfer(_Type):
mts: int
@@ -418,6 +454,7 @@ class Transfer(_Type):
currency_to: str
amount: int
@dataclass
class Withdrawal(_Type):
withdrawal_id: int
@@ -427,6 +464,7 @@ class Withdrawal(_Type):
amount: float
withdrawal_fee: float
@dataclass
class DepositAddress(_Type):
method: str
@@ -434,12 +472,14 @@ class DepositAddress(_Type):
address: str
pool_address: str
@dataclass
class LightningNetworkInvoice(_Type):
invoice_hash: str
invoice: str
amount: str
@dataclass
class Movement(_Type):
id: str
@@ -454,6 +494,7 @@ class Movement(_Type):
transaction_id: str
withdraw_transaction_note: str
@dataclass
class SymbolMarginInfo(_Type):
symbol: str
@@ -462,6 +503,7 @@ class SymbolMarginInfo(_Type):
buy: float
sell: float
@dataclass
class BaseMarginInfo(_Type):
user_pl: float
@@ -470,6 +512,7 @@ class BaseMarginInfo(_Type):
margin_net: float
margin_min: float
@dataclass
class PositionClaim(_Type):
symbol: str
@@ -486,6 +529,7 @@ class PositionClaim(_Type):
min_collateral: str
meta: Dict[str, Any]
@dataclass
class PositionIncreaseInfo(_Type):
max_pos: int
@@ -501,12 +545,14 @@ class PositionIncreaseInfo(_Type):
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
@@ -519,6 +565,7 @@ class PositionHistory(_Type):
mts_create: int
mts_update: int
@dataclass
class PositionSnapshot(_Type):
symbol: str
@@ -531,6 +578,7 @@ class PositionSnapshot(_Type):
mts_create: int
mts_update: int
@dataclass
class PositionAudit(_Type):
symbol: str
@@ -547,18 +595,22 @@ class PositionAudit(_Type):
collateral_min: float
meta: Dict[str, Any]
@dataclass
class DerivativePositionCollateral(_Type):
status: int
@dataclass
class DerivativePositionCollateralLimits(_Type):
min_collateral: float
max_collateral: float
#endregion
#region Dataclass definitions for types of merchant use
# endregion
# region Dataclass definitions for types of merchant use
@compose(dataclass, partial)
class InvoiceSubmission(_Type):
@@ -582,7 +634,9 @@ class InvoiceSubmission(_Type):
@classmethod
def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission":
if "customer_info" in data and data["customer_info"] is not None:
data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"])
data["customer_info"] = InvoiceSubmission.CustomerInfo(
**data["customer_info"]
)
for index, invoice in enumerate(data["invoices"]):
data["invoices"][index] = InvoiceSubmission.Invoice(**invoice)
@@ -592,7 +646,9 @@ class InvoiceSubmission(_Type):
if "additional_payments" in data and data["additional_payments"] is not None:
for index, additional_payment in enumerate(data["additional_payments"]):
data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment)
data["additional_payments"][index] = InvoiceSubmission.Payment(
**additional_payment
)
return InvoiceSubmission(**data)
@@ -633,6 +689,7 @@ class InvoiceSubmission(_Type):
force_completed: bool
amount_diff: str
@dataclass
class InvoicePage(_Type):
page: int
@@ -650,17 +707,20 @@ class InvoicePage(_Type):
return InvoicePage(**data)
@dataclass
class InvoiceStats(_Type):
time: str
count: float
@dataclass
class CurrencyConversion(_Type):
base_ccy: str
convert_ccy: str
created: int
@dataclass
class MerchantDeposit(_Type):
id: int
@@ -674,6 +734,7 @@ class MerchantDeposit(_Type):
method: str
pay_method: str
@dataclass
class MerchantUnlinkedDeposit(_Type):
id: int
@@ -689,4 +750,5 @@ class MerchantUnlinkedDeposit(_Type):
status: str
note: Optional[str]
#endregion
# endregion

View File

@@ -1,8 +1,8 @@
from typing import Type, Generic, TypeVar, Iterable, \
Dict, List, Tuple, Any, cast
from typing import Any, Dict, Generic, Iterable, List, Tuple, Type, TypeVar, cast
T = TypeVar("T", bound="_Type")
def compose(*decorators):
def wrapper(function):
for decorator in reversed(decorators):
@@ -11,30 +11,38 @@ def compose(*decorators):
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])
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]}'")
raise TypeError(
f"{cls.__name__}.__init__() got an unexpected "
"keyword argument '{list(kwargs.keys())[0]}'"
)
cls.__init__ = __init__
return cls
class _Type:
"""
Base class for any dataclass serializable by the _Serializer generic class.
"""
class _Serializer(Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str],
*, flat: bool = False):
def __init__(
self, name: str, klass: Type[_Type], labels: List[str], *, flat: bool = False
):
self.name, self.klass, self.__labels, self.__flat = name, klass, labels, flat
def _serialize(self, *args: Any) -> Iterable[Tuple[str, Any]]:
@@ -42,8 +50,10 @@ class _Serializer(Generic[T]):
args = tuple(_Serializer.__flatten(list(args)))
if len(self.__labels) > len(args):
raise AssertionError(f"{self.name} -> <labels> and <*args> " \
"arguments should contain the same amount of elements.")
raise AssertionError(
f"{self.name} -> <labels> and <*args> "
"arguments should contain the same amount of elements."
)
for index, label in enumerate(self.__labels):
if label != "_PLACEHOLDER":
@@ -53,7 +63,7 @@ class _Serializer(Generic[T]):
return cast(T, self.klass(**dict(self._serialize(*values))))
def get_labels(self) -> List[str]:
return [ label for label in self.__labels if label != "_PLACEHOLDER" ]
return [label for label in self.__labels if label != "_PLACEHOLDER"]
@classmethod
def __flatten(cls, array: List[Any]) -> List[Any]:
@@ -65,10 +75,17 @@ class _Serializer(Generic[T]):
return array[:1] + cls.__flatten(array[1:])
class _RecursiveSerializer(_Serializer, Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str],
*, serializers: Dict[str, _Serializer[Any]],
flat: bool = False):
def __init__(
self,
name: str,
klass: Type[_Type],
labels: List[str],
*,
serializers: Dict[str, _Serializer[Any]],
flat: bool = False,
):
super().__init__(name, klass, labels, flat=flat)
self.serializers = serializers
@@ -82,15 +99,21 @@ class _RecursiveSerializer(_Serializer, Generic[T]):
return cast(T, self.klass(**serialization))
def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str],
*, flat: bool = False
) -> _Serializer[T]:
return _Serializer[T](name, klass, labels, \
flat=flat)
def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str],
*, serializers: Dict[str, _Serializer[Any]],
flat: bool = False
) -> _RecursiveSerializer[T]:
return _RecursiveSerializer[T](name, klass, labels, \
serializers=serializers, flat=flat)
def generate_labeler_serializer(
name: str, klass: Type[T], labels: List[str], *, flat: bool = False
) -> _Serializer[T]:
return _Serializer[T](name, klass, labels, flat=flat)
def generate_recursive_serializer(
name: str,
klass: Type[T],
labels: List[str],
*,
serializers: Dict[str, _Serializer[Any]],
flat: bool = False,
) -> _RecursiveSerializer[T]:
return _RecursiveSerializer[T](
name, klass, labels, serializers=serializers, flat=flat
)

View File

@@ -1,9 +1,11 @@
from typing import List, Optional, Any, Generic, TypeVar, cast
from dataclasses import dataclass
from .labeler import _Type, _Serializer
from typing import Any, Generic, List, Optional, TypeVar, cast
from .labeler import _Serializer, _Type
T = TypeVar("T")
@dataclass
class Notification(_Type, Generic[T]):
mts: int
@@ -14,16 +16,30 @@ class Notification(_Type, Generic[T]):
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):
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)
self.serializer, self.is_iterable = serializer, is_iterable
def parse(self, *values: Any) -> Notification[T]:
notification = cast(Notification[T], Notification(**dict(self._serialize(*values))))
notification = cast(
Notification[T], Notification(**dict(self._serialize(*values)))
)
if isinstance(self.serializer, _Serializer):
data = cast(List[Any], notification.data)
@@ -33,6 +49,9 @@ class _Notification(_Serializer, Generic[T]):
data = data[0]
notification.data = self.serializer.parse(*data)
else: notification.data = cast(T, [ self.serializer.parse(*sub_data) for sub_data in data ])
else:
notification.data = cast(
T, [self.serializer.parse(*sub_data) for sub_data in data]
)
return notification

View File

@@ -1,41 +1,69 @@
from .import dataclasses
#pylint: disable-next=unused-import
from .labeler import _Serializer, \
generate_labeler_serializer, generate_recursive_serializer
#pylint: disable-next=unused-import
from .notification import _Notification
from . import dataclasses
from .labeler import ( # noqa: F401
_Serializer,
generate_labeler_serializer,
generate_recursive_serializer,
)
from .notification import _Notification # noqa: F401
__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",
"PlatformStatus",
"TradingPairTicker",
"FundingCurrencyTicker",
"TickersHistory",
"TradingPairTrade",
"FundingCurrencyTrade",
"TradingPairBook",
"FundingCurrencyBook",
"TradingPairRawBook",
"FundingCurrencyRawBook",
"Statistic",
"Candle",
"DerivativesStatus",
"Liquidation",
"Leaderboard",
"FundingStatistic",
"PulseProfile",
"PulseMessage",
"TradingMarketAveragePrice",
"FundingMarketAveragePrice",
"FxRate",
"UserInfo",
"LoginHistory",
"BalanceAvailable",
"Order",
"Position",
"Trade",
"FundingTrade",
"OrderTrade",
"Ledger",
"FundingOffer",
"FundingCredit",
"FundingLoan",
"FundingAutoRenew",
"FundingInfo",
"Wallet",
"Transfer",
"Withdrawal",
"DepositAddress",
"LightningNetworkInvoice",
"Movement",
"SymbolMarginInfo",
"BaseMarginInfo",
"PositionClaim",
"PositionIncreaseInfo",
"PositionIncrease",
"PositionHistory",
"PositionSnapshot",
"PositionAudit",
"DerivativePositionCollateral",
"DerivativePositionCollateralLimits",
]
#region Serializer definitions for types of public use
# region Serializer definitions for types of public use
PlatformStatus = generate_labeler_serializer(
name="PlatformStatus",
klass=dataclasses.PlatformStatus,
labels=[
"status"
]
name="PlatformStatus", klass=dataclasses.PlatformStatus, labels=["status"]
)
TradingPairTicker = generate_labeler_serializer(
@@ -51,8 +79,8 @@ TradingPairTicker = generate_labeler_serializer(
"last_price",
"volume",
"high",
"low"
]
"low",
],
)
FundingCurrencyTicker = generate_labeler_serializer(
@@ -74,8 +102,8 @@ FundingCurrencyTicker = generate_labeler_serializer(
"low",
"_PLACEHOLDER",
"_PLACEHOLDER",
"frr_amount_available"
]
"frr_amount_available",
],
)
TickersHistory = generate_labeler_serializer(
@@ -94,95 +122,54 @@ TickersHistory = generate_labeler_serializer(
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mts"
]
"mts",
],
)
TradingPairTrade = generate_labeler_serializer(
name="TradingPairTrade",
klass=dataclasses.TradingPairTrade,
labels=[
"id",
"mts",
"amount",
"price"
]
labels=["id", "mts", "amount", "price"],
)
FundingCurrencyTrade = generate_labeler_serializer(
name="FundingCurrencyTrade",
klass=dataclasses.FundingCurrencyTrade,
labels=[
"id",
"mts",
"amount",
"rate",
"period"
]
labels=["id", "mts", "amount", "rate", "period"],
)
TradingPairBook = generate_labeler_serializer(
name="TradingPairBook",
klass=dataclasses.TradingPairBook,
labels=[
"price",
"count",
"amount"
]
labels=["price", "count", "amount"],
)
FundingCurrencyBook = generate_labeler_serializer(
name="FundingCurrencyBook",
klass=dataclasses.FundingCurrencyBook,
labels=[
"rate",
"period",
"count",
"amount"
]
labels=["rate", "period", "count", "amount"],
)
TradingPairRawBook = generate_labeler_serializer(
name="TradingPairRawBook",
klass=dataclasses.TradingPairRawBook,
labels=[
"order_id",
"price",
"amount"
]
labels=["order_id", "price", "amount"],
)
FundingCurrencyRawBook = generate_labeler_serializer(
name="FundingCurrencyRawBook",
klass=dataclasses.FundingCurrencyRawBook,
labels=[
"offer_id",
"period",
"rate",
"amount"
]
labels=["offer_id", "period", "rate", "amount"],
)
Statistic = generate_labeler_serializer(
name="Statistic",
klass=dataclasses.Statistic,
labels=[
"mts",
"value"
]
name="Statistic", klass=dataclasses.Statistic, labels=["mts", "value"]
)
Candle = generate_labeler_serializer(
name="Candle",
klass=dataclasses.Candle,
labels=[
"mts",
"open",
"close",
"high",
"low",
"volume"
]
labels=["mts", "open", "close", "high", "low", "volume"],
)
DerivativesStatus = generate_labeler_serializer(
@@ -190,7 +177,7 @@ DerivativesStatus = generate_labeler_serializer(
klass=dataclasses.DerivativesStatus,
labels=[
"mts",
"_PLACEHOLDER",
"_PLACEHOLDER",
"deriv_price",
"spot_price",
"_PLACEHOLDER",
@@ -211,8 +198,8 @@ DerivativesStatus = generate_labeler_serializer(
"_PLACEHOLDER",
"_PLACEHOLDER",
"clamp_min",
"clamp_max"
]
"clamp_max",
],
)
Liquidation = generate_labeler_serializer(
@@ -230,8 +217,8 @@ Liquidation = generate_labeler_serializer(
"is_match",
"is_market_sold",
"_PLACEHOLDER",
"liquidation_price"
]
"liquidation_price",
],
)
Leaderboard = generate_labeler_serializer(
@@ -247,8 +234,8 @@ Leaderboard = generate_labeler_serializer(
"value",
"_PLACEHOLDER",
"_PLACEHOLDER",
"twitter_handle"
]
"twitter_handle",
],
)
FundingStatistic = generate_labeler_serializer(
@@ -266,8 +253,8 @@ FundingStatistic = generate_labeler_serializer(
"funding_amount_used",
"_PLACEHOLDER",
"_PLACEHOLDER",
"funding_below_threshold"
]
"funding_below_threshold",
],
)
PulseProfile = generate_labeler_serializer(
@@ -290,14 +277,14 @@ PulseProfile = generate_labeler_serializer(
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"tipping_status"
]
"tipping_status",
],
)
PulseMessage = generate_recursive_serializer(
name="PulseMessage",
klass=dataclasses.PulseMessage,
serializers={ "profile": PulseProfile },
serializers={"profile": PulseProfile},
labels=[
"pid",
"mts",
@@ -311,7 +298,7 @@ PulseMessage = generate_recursive_serializer(
"is_pin",
"is_public",
"comments_disabled",
"tags",
"tags",
"attachments",
"meta",
"likes",
@@ -320,39 +307,29 @@ PulseMessage = generate_recursive_serializer(
"profile",
"comments",
"_PLACEHOLDER",
"_PLACEHOLDER"
]
"_PLACEHOLDER",
],
)
TradingMarketAveragePrice = generate_labeler_serializer(
name="TradingMarketAveragePrice",
klass=dataclasses.TradingMarketAveragePrice,
labels=[
"price_avg",
"amount"
]
labels=["price_avg", "amount"],
)
FundingMarketAveragePrice = generate_labeler_serializer(
name="FundingMarketAveragePrice",
klass=dataclasses.FundingMarketAveragePrice,
labels=[
"rate_avg",
"amount"
]
labels=["rate_avg", "amount"],
)
FxRate = generate_labeler_serializer(
name="FxRate",
klass=dataclasses.FxRate,
labels=[
"current_rate"
]
name="FxRate", klass=dataclasses.FxRate, labels=["current_rate"]
)
#endregion
# endregion
#region Serializer definitions for types of auth use
# region Serializer definitions for types of auth use
UserInfo = generate_labeler_serializer(
name="UserInfo",
@@ -412,8 +389,8 @@ UserInfo = generate_labeler_serializer(
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_merchant_enterprise"
]
"is_merchant_enterprise",
],
)
LoginHistory = generate_labeler_serializer(
@@ -427,16 +404,12 @@ LoginHistory = generate_labeler_serializer(
"ip",
"_PLACEHOLDER",
"_PLACEHOLDER",
"extra_info"
]
"extra_info",
],
)
BalanceAvailable = generate_labeler_serializer(
name="BalanceAvailable",
klass=dataclasses.BalanceAvailable,
labels=[
"amount"
]
name="BalanceAvailable", klass=dataclasses.BalanceAvailable, labels=["amount"]
)
Order = generate_labeler_serializer(
@@ -447,10 +420,10 @@ Order = generate_labeler_serializer(
"gid",
"cid",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"order_type",
"type_prev",
"mts_tif",
@@ -467,26 +440,26 @@ Order = generate_labeler_serializer(
"_PLACEHOLDER",
"_PLACEHOLDER",
"notify",
"hidden",
"hidden",
"placed_id",
"_PLACEHOLDER",
"_PLACEHOLDER",
"routing",
"_PLACEHOLDER",
"_PLACEHOLDER",
"meta"
]
"meta",
],
)
Position = generate_labeler_serializer(
name="Position",
klass=dataclasses.Position,
labels=[
"symbol",
"status",
"amount",
"base_price",
"margin_funding",
"symbol",
"status",
"amount",
"base_price",
"margin_funding",
"margin_funding_type",
"pl",
"pl_perc",
@@ -501,41 +474,33 @@ Position = generate_labeler_serializer(
"_PLACEHOLDER",
"collateral",
"collateral_min",
"meta"
]
"meta",
],
)
Trade = generate_labeler_serializer(
name="Trade",
klass=dataclasses.Trade,
labels=[
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"order_type",
"order_price",
"maker",
"fee",
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"order_type",
"order_price",
"maker",
"fee",
"fee_currency",
"cid"
]
"cid",
],
)
FundingTrade = generate_labeler_serializer(
name="FundingTrade",
klass=dataclasses.FundingTrade,
labels=[
"id",
"currency",
"mts_create",
"offer_id",
"amount",
"rate",
"period"
]
labels=["id", "currency", "mts_create", "offer_id", "amount", "rate", "period"],
)
OrderTrade = generate_labeler_serializer(
@@ -553,8 +518,8 @@ OrderTrade = generate_labeler_serializer(
"maker",
"fee",
"fee_currency",
"cid"
]
"cid",
],
)
Ledger = generate_labeler_serializer(
@@ -569,8 +534,8 @@ Ledger = generate_labeler_serializer(
"amount",
"balance",
"_PLACEHOLDER",
"description"
]
"description",
],
)
FundingOffer = generate_labeler_serializer(
@@ -597,8 +562,8 @@ FundingOffer = generate_labeler_serializer(
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER"
]
"_PLACEHOLDER",
],
)
FundingCredit = generate_labeler_serializer(
@@ -626,8 +591,8 @@ FundingCredit = generate_labeler_serializer(
"renew",
"_PLACEHOLDER",
"no_close",
"position_pair"
]
"position_pair",
],
)
FundingLoan = generate_labeler_serializer(
@@ -654,44 +619,34 @@ FundingLoan = generate_labeler_serializer(
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER",
"no_close"
]
"no_close",
],
)
FundingAutoRenew = generate_labeler_serializer(
name="FundingAutoRenew",
klass=dataclasses.FundingAutoRenew,
labels=[
"currency",
"period",
"rate",
"threshold"
]
labels=["currency", "period", "rate", "threshold"],
)
FundingInfo = generate_labeler_serializer(
name="FundingInfo",
klass=dataclasses.FundingInfo,
labels=[
"yield_loan",
"yield_lend",
"duration_loan",
"duration_lend"
]
labels=["yield_loan", "yield_lend", "duration_loan", "duration_lend"],
)
Wallet = generate_labeler_serializer(
name="Wallet",
klass=dataclasses.Wallet,
labels=[
"wallet_type",
"currency",
"balance",
"wallet_type",
"currency",
"balance",
"unsettled_interest",
"available_balance",
"last_change",
"trade_details"
]
"trade_details",
],
)
Transfer = generate_labeler_serializer(
@@ -705,8 +660,8 @@ Transfer = generate_labeler_serializer(
"currency",
"currency_to",
"_PLACEHOLDER",
"amount"
]
"amount",
],
)
Withdrawal = generate_labeler_serializer(
@@ -721,8 +676,8 @@ Withdrawal = generate_labeler_serializer(
"amount",
"_PLACEHOLDER",
"_PLACEHOLDER",
"withdrawal_fee"
]
"withdrawal_fee",
],
)
DepositAddress = generate_labeler_serializer(
@@ -734,20 +689,14 @@ DepositAddress = generate_labeler_serializer(
"currency_code",
"_PLACEHOLDER",
"address",
"pool_address"
]
"pool_address",
],
)
LightningNetworkInvoice = generate_labeler_serializer(
name="LightningNetworkInvoice",
klass=dataclasses.LightningNetworkInvoice,
labels=[
"invoice_hash",
"invoice",
"_PLACEHOLDER",
"_PLACEHOLDER",
"amount"
]
labels=["invoice_hash", "invoice", "_PLACEHOLDER", "_PLACEHOLDER", "amount"],
)
Movement = generate_labeler_serializer(
@@ -775,8 +724,8 @@ Movement = generate_labeler_serializer(
"_PLACEHOLDER",
"_PLACEHOLDER",
"transaction_id",
"withdraw_transaction_note"
]
"withdraw_transaction_note",
],
)
SymbolMarginInfo = generate_labeler_serializer(
@@ -788,22 +737,15 @@ SymbolMarginInfo = generate_labeler_serializer(
"tradable_balance",
"gross_balance",
"buy",
"sell"
"sell",
],
flat=True
flat=True,
)
BaseMarginInfo = generate_labeler_serializer(
name="BaseMarginInfo",
klass=dataclasses.BaseMarginInfo,
labels=[
"user_pl",
"user_swaps",
"margin_balance",
"margin_net",
"margin_min"
]
labels=["user_pl", "user_swaps", "margin_balance", "margin_net", "margin_min"],
)
PositionClaim = generate_labeler_serializer(
@@ -829,8 +771,8 @@ PositionClaim = generate_labeler_serializer(
"_PLACEHOLDER",
"collateral",
"min_collateral",
"meta"
]
"meta",
],
)
PositionIncreaseInfo = generate_labeler_serializer(
@@ -854,21 +796,15 @@ PositionIncreaseInfo = generate_labeler_serializer(
"funding_value",
"funding_required",
"funding_value_currency",
"funding_required_currency"
"funding_required_currency",
],
flat=True
flat=True,
)
PositionIncrease = generate_labeler_serializer(
name="PositionIncrease",
klass=dataclasses.PositionIncrease,
labels=[
"symbol",
"_PLACEHOLDER",
"amount",
"base_price"
]
labels=["symbol", "_PLACEHOLDER", "amount", "base_price"],
)
PositionHistory = generate_labeler_serializer(
@@ -888,8 +824,8 @@ PositionHistory = generate_labeler_serializer(
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update"
]
"mts_update",
],
)
PositionSnapshot = generate_labeler_serializer(
@@ -909,8 +845,8 @@ PositionSnapshot = generate_labeler_serializer(
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update"
]
"mts_update",
],
)
PositionAudit = generate_labeler_serializer(
@@ -936,25 +872,20 @@ PositionAudit = generate_labeler_serializer(
"_PLACEHOLDER",
"collateral",
"collateral_min",
"meta"
]
"meta",
],
)
DerivativePositionCollateral = generate_labeler_serializer(
name="DerivativePositionCollateral",
klass=dataclasses.DerivativePositionCollateral,
labels=[
"status"
]
labels=["status"],
)
DerivativePositionCollateralLimits = generate_labeler_serializer(
name="DerivativePositionCollateralLimits",
klass=dataclasses.DerivativePositionCollateralLimits,
labels=[
"min_collateral",
"max_collateral"
]
labels=["min_collateral", "max_collateral"],
)
#endregion
# endregion

View File

@@ -1,23 +1,22 @@
from typing import \
List, Dict, Any, \
Optional, cast
import asyncio, json, uuid
import asyncio
import json
import uuid
from typing import Any, Dict, List, Optional, cast
import websockets.client
from pyee import EventEmitter
from bfxapi._utils.json_decoder import JSONDecoder
from bfxapi.websocket._connection import Connection
from bfxapi.websocket._handlers import PublicChannelsHandler
from bfxapi.websocket.subscriptions import Subscription
_CHECKSUM_FLAG_VALUE = 131_072
def _strip(message: Dict[str, Any], keys: List[str]) -> Dict[str, Any]:
return { key: value for key, value in message.items() \
if not key in keys }
return {key: value for key, value in message.items() if key not in keys}
class BfxWebSocketBucket(Connection):
__MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
@@ -26,28 +25,26 @@ class BfxWebSocketBucket(Connection):
super().__init__(host)
self.__event_emitter = event_emitter
self.__pendings: List[Dict[str, Any]] = [ ]
self.__subscriptions: Dict[int, Subscription] = { }
self.__pendings: List[Dict[str, Any]] = []
self.__subscriptions: Dict[int, Subscription] = {}
self.__condition = asyncio.locks.Condition()
self.__handler = PublicChannelsHandler( \
event_emitter=self.__event_emitter)
self.__handler = PublicChannelsHandler(event_emitter=self.__event_emitter)
@property
def count(self) -> int:
return len(self.__pendings) + \
len(self.__subscriptions)
return len(self.__pendings) + len(self.__subscriptions)
@property
def is_full(self) -> bool:
return self.count == \
BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT
return self.count == BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT
@property
def ids(self) -> List[str]:
return [ pending["subId"] for pending in self.__pendings ] + \
[ subscription["sub_id"] for subscription in self.__subscriptions.values() ]
return [pending["subId"] for pending in self.__pendings] + [
subscription["sub_id"] for subscription in self.__subscriptions.values()
]
async def start(self) -> None:
async with websockets.client.connect(self._host) as websocket:
@@ -66,20 +63,25 @@ class BfxWebSocketBucket(Connection):
self.__on_subscribed(message)
if isinstance(message, list):
if (chan_id := cast(int, message[0])) and \
(subscription := self.__subscriptions.get(chan_id)) and \
(message[1] != Connection._HEARTBEAT):
if (
(chan_id := cast(int, message[0]))
and (subscription := self.__subscriptions.get(chan_id))
and (message[1] != Connection._HEARTBEAT)
):
self.__handler.handle(subscription, message[1:])
def __on_subscribed(self, message: Dict[str, Any]) -> None:
chan_id = cast(int, message["chan_id"])
subscription = cast(Subscription, _strip(message, \
keys=["chan_id", "event", "pair", "currency"]))
subscription = cast(
Subscription, _strip(message, keys=["chan_id", "event", "pair", "currency"])
)
self.__pendings = [ pending \
for pending in self.__pendings \
if pending["subId"] != message["sub_id"] ]
self.__pendings = [
pending
for pending in self.__pendings
if pending["subId"] != message["sub_id"]
]
self.__subscriptions[chan_id] = subscription
@@ -87,47 +89,43 @@ class BfxWebSocketBucket(Connection):
async def __recover_state(self) -> None:
for pending in self.__pendings:
await self._websocket.send(message = \
json.dumps(pending))
await self._websocket.send(message=json.dumps(pending))
for chan_id in list(self.__subscriptions.keys()):
subscription = self.__subscriptions.pop(chan_id)
await self.subscribe(**subscription)
await self.__set_config([ _CHECKSUM_FLAG_VALUE ])
await self.__set_config([_CHECKSUM_FLAG_VALUE])
async def __set_config(self, flags: List[int]) -> None:
await self._websocket.send(json.dumps( \
{ "event": "conf", "flags": sum(flags) }))
await self._websocket.send(json.dumps({"event": "conf", "flags": sum(flags)}))
@Connection._require_websocket_connection
async def subscribe(self,
channel: str,
sub_id: Optional[str] = None,
**kwargs: Any) -> None:
subscription: Dict[str, Any] = \
{ **kwargs, "event": "subscribe", "channel": channel }
async def subscribe(
self, channel: str, sub_id: Optional[str] = None, **kwargs: Any
) -> None:
subscription: Dict[str, Any] = {
**kwargs,
"event": "subscribe",
"channel": channel,
}
subscription["subId"] = sub_id or str(uuid.uuid4())
self.__pendings.append(subscription)
await self._websocket.send(message = \
json.dumps(subscription))
await self._websocket.send(message=json.dumps(subscription))
@Connection._require_websocket_connection
async def unsubscribe(self, sub_id: str) -> None:
for chan_id, subscription in list(self.__subscriptions.items()):
if subscription["sub_id"] == sub_id:
unsubscription = {
"event": "unsubscribe",
"chanId": chan_id }
unsubscription = {"event": "unsubscribe", "chanId": chan_id}
del self.__subscriptions[chan_id]
await self._websocket.send(message = \
json.dumps(unsubscription))
await self._websocket.send(message=json.dumps(unsubscription))
@Connection._require_websocket_connection
async def resubscribe(self, sub_id: str) -> None:
@@ -138,7 +136,7 @@ class BfxWebSocketBucket(Connection):
await self.subscribe(**subscription)
@Connection._require_websocket_connection
async def close(self, code: int = 1000, reason: str = str()) -> None:
async def close(self, code: int = 1000, reason: str = "") -> None:
await self._websocket.close(code, reason)
def has(self, sub_id: str) -> bool:
@@ -150,5 +148,4 @@ class BfxWebSocketBucket(Connection):
async def wait(self) -> None:
async with self.__condition:
await self.__condition \
.wait_for(lambda: self.open)
await self.__condition.wait_for(lambda: self.open)

View File

@@ -1,51 +1,44 @@
from typing import \
TypedDict, List, Dict, \
Optional, Any
from logging import Logger
from datetime import datetime
from socket import gaierror
import asyncio
import json
import random
import traceback
from asyncio import Task
from datetime import datetime
from logging import Logger
from socket import gaierror
from typing import Any, Dict, List, Optional, TypedDict
import \
traceback, json, asyncio, \
random, websockets
import websockets
import websockets.client
from websockets.exceptions import \
ConnectionClosedError, \
InvalidStatusCode
from websockets.exceptions import ConnectionClosedError, InvalidStatusCode
from bfxapi._utils.json_encoder import JSONEncoder
from bfxapi.exceptions import InvalidCredentialError
from bfxapi.websocket._connection import Connection
from bfxapi.websocket._handlers import AuthEventsHandler
from bfxapi.websocket._event_emitter import BfxEventEmitter
from bfxapi.exceptions import \
InvalidCredentialError
from bfxapi.websocket.exceptions import \
ReconnectionTimeoutError, \
VersionMismatchError, \
UnknownChannelError, \
UnknownSubscriptionError, \
SubIdError
from bfxapi.websocket._handlers import AuthEventsHandler
from bfxapi.websocket.exceptions import (
ReconnectionTimeoutError,
SubIdError,
UnknownChannelError,
UnknownSubscriptionError,
VersionMismatchError,
)
from .bfx_websocket_bucket import BfxWebSocketBucket
from .bfx_websocket_inputs import BfxWebSocketInputs
_Credentials = TypedDict("_Credentials", \
{ "api_key": str, "api_secret": str, "filters": Optional[List[str]] })
_Credentials = TypedDict(
"_Credentials", {"api_key": str, "api_secret": str, "filters": Optional[List[str]]}
)
_Reconnection = TypedDict("_Reconnection",
{ "attempts": int, "reason": str, "timestamp": datetime })
_Reconnection = TypedDict(
"_Reconnection", {"attempts": int, "reason": str, "timestamp": datetime}
)
_DEFAULT_LOGGER = Logger("bfxapi.websocket._client", level=0)
class _Delay:
__BACKOFF_MIN = 1.92
@@ -64,59 +57,58 @@ class _Delay:
return _backoff_delay
def peek(self) -> float:
return (self.__backoff_delay == _Delay.__BACKOFF_MIN) \
and self.__initial_delay or self.__backoff_delay
return (
(self.__backoff_delay == _Delay.__BACKOFF_MIN)
and self.__initial_delay
or self.__backoff_delay
)
def reset(self) -> None:
self.__backoff_delay = _Delay.__BACKOFF_MIN
#pylint: disable-next=too-many-instance-attributes
class BfxWebSocketClient(Connection):
def __init__(self,
host: str,
*,
credentials: Optional[_Credentials] = None,
timeout: Optional[int] = 60 * 15,
logger: Logger = _DEFAULT_LOGGER) -> None:
def __init__(
self,
host: str,
*,
credentials: Optional[_Credentials] = None,
timeout: Optional[int] = 60 * 15,
logger: Logger = _DEFAULT_LOGGER,
) -> None:
super().__init__(host)
self.__credentials, self.__timeout, self.__logger = \
credentials, \
timeout, \
logger
self.__credentials, self.__timeout, self.__logger = credentials, timeout, logger
self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = { }
self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = {}
self.__reconnection: Optional[_Reconnection] = None
self.__event_emitter = BfxEventEmitter(loop=None)
self.__handler = AuthEventsHandler( \
event_emitter=self.__event_emitter)
self.__handler = AuthEventsHandler(event_emitter=self.__event_emitter)
self.__inputs = BfxWebSocketInputs( \
handle_websocket_input=self.__handle_websocket_input)
self.__inputs = BfxWebSocketInputs(
handle_websocket_input=self.__handle_websocket_input
)
@self.__event_emitter.listens_to("error")
def error(exception: Exception) -> None:
header = f"{type(exception).__name__}: {str(exception)}"
stack_trace = traceback.format_exception( \
type(exception), exception, exception.__traceback__)
stack_trace = traceback.format_exception(
type(exception), exception, exception.__traceback__
)
#pylint: disable-next=logging-not-lazy
self.__logger.critical(header + "\n" + \
str().join(stack_trace)[:-1])
self.__logger.critical(f"{header}\n" + str().join(stack_trace)[:-1])
@property
def inputs(self) -> BfxWebSocketInputs:
return self.__inputs
def run(self) -> None:
return asyncio.get_event_loop() \
.run_until_complete(self.start())
return asyncio.get_event_loop().run_until_complete(self.start())
#pylint: disable-next=too-many-branches
async def start(self) -> None:
_delay = _Delay(backoff_factor=1.618)
@@ -129,100 +121,117 @@ class BfxWebSocketClient(Connection):
while True:
if self.__reconnection:
_sleep = asyncio.create_task( \
asyncio.sleep(int(_delay.next())))
_sleep = asyncio.create_task(asyncio.sleep(int(_delay.next())))
try:
await _sleep
except asyncio.CancelledError:
raise ReconnectionTimeoutError("Connection has been offline for too long " \
f"without being able to reconnect (timeout: {self.__timeout}s).") \
from None
raise ReconnectionTimeoutError(
"Connection has been offline for too long "
f"without being able to reconnect (timeout: {self.__timeout}s)."
) from None
try:
await self.__connect()
except (ConnectionClosedError, InvalidStatusCode, gaierror) as error:
async def _cancel(task: Task) -> None:
task.cancel()
try:
await task
except (ConnectionClosedError, InvalidStatusCode, gaierror) as _e:
nonlocal error
if type(error) is not type(_e) or error.args != _e.args:
raise _e
except asyncio.CancelledError:
pass
# pylint: disable-next=consider-using-dict-items
for bucket in self.__buckets:
if task := self.__buckets[bucket]:
self.__buckets[bucket] = None
await _cancel(task)
if isinstance(error, ConnectionClosedError) and error.code in (1006, 1012):
if isinstance(error, ConnectionClosedError) and error.code in (
1006,
1012,
):
if error.code == 1006:
self.__logger.error("Connection lost: trying to reconnect...")
if error.code == 1012:
self.__logger.warning("WSS server is restarting: all " \
"clients need to reconnect (server sent 20051).")
self.__logger.warning(
"WSS server is restarting: all "
"clients need to reconnect (server sent 20051)."
)
if self.__timeout:
asyncio.get_event_loop().call_later(
self.__timeout, _on_timeout)
asyncio.get_event_loop().call_later(self.__timeout, _on_timeout)
self.__reconnection = \
{ "attempts": 1, "reason": error.reason, "timestamp": datetime.now() }
self.__reconnection = {
"attempts": 1,
"reason": error.reason,
"timestamp": datetime.now(),
}
self._authentication = False
_delay.reset()
elif ((isinstance(error, InvalidStatusCode) and error.status_code == 408) or \
isinstance(error, gaierror)) and self.__reconnection:
#pylint: disable-next=logging-fstring-interpolation
self.__logger.warning("Reconnection attempt unsuccessful (no." \
f"{self.__reconnection['attempts']}): next attempt in " \
f"~{int(_delay.peek())}.0s.")
elif (
(isinstance(error, InvalidStatusCode) and error.status_code == 408)
or isinstance(error, gaierror)
) and self.__reconnection:
self.__logger.warning(
"Reconnection attempt unsuccessful (no."
f"{self.__reconnection['attempts']}): next attempt in "
f"~{int(_delay.peek())}.0s."
)
#pylint: disable-next=logging-fstring-interpolation
self.__logger.info(f"The client has been offline for " \
f"{datetime.now() - self.__reconnection['timestamp']}.")
self.__logger.info(
f"The client has been offline for "
f"{datetime.now() - self.__reconnection['timestamp']}."
)
self.__reconnection["attempts"] += 1
else:
raise error
if not self.__reconnection:
self.__event_emitter.emit("disconnected",
self._websocket.close_code, \
self._websocket.close_reason)
self.__event_emitter.emit(
"disconnected",
self._websocket.close_code,
self._websocket.close_reason,
)
break
async def __connect(self) -> None:
async with websockets.client.connect(self._host) as websocket:
if self.__reconnection:
#pylint: disable-next=logging-fstring-interpolation
self.__logger.warning("Reconnection attempt successful (no." \
f"{self.__reconnection['attempts']}): recovering " \
"connection state...")
self.__logger.warning(
"Reconnection attempt successful (no."
f"{self.__reconnection['attempts']}): recovering "
"connection state..."
)
self.__reconnection = None
self._websocket = websocket
for bucket in self.__buckets:
self.__buckets[bucket] = \
asyncio.create_task(bucket.start())
self.__buckets[bucket] = asyncio.create_task(bucket.start())
if len(self.__buckets) == 0 or \
(await asyncio.gather(*[bucket.wait() for bucket in self.__buckets])):
if len(self.__buckets) == 0 or (
await asyncio.gather(*[bucket.wait() for bucket in self.__buckets])
):
self.__event_emitter.emit("open")
if self.__credentials:
authentication = Connection. \
_get_authentication_message(**self.__credentials)
authentication = Connection._get_authentication_message(
**self.__credentials
)
await self._websocket.send(authentication)
@@ -232,77 +241,79 @@ class BfxWebSocketClient(Connection):
if isinstance(message, dict):
if message["event"] == "info" and "version" in message:
if message["version"] != 2:
raise VersionMismatchError("Mismatch between the client and the server version: " + \
"please update bitfinex-api-py to the latest version to resolve this error " + \
f"(client version: 2, server version: {message['version']}).")
raise VersionMismatchError(
"Mismatch between the client and the server version: "
"please update bitfinex-api-py to the latest version "
f"to resolve this error (client version: 2, server "
f"version: {message['version']})."
)
elif message["event"] == "info" and message["code"] == 20051:
rcvd = websockets.frames.Close( \
1012, "Stop/Restart WebSocket Server (please reconnect).")
rcvd = websockets.frames.Close(
1012, "Stop/Restart WebSocket Server (please reconnect)."
)
raise ConnectionClosedError(rcvd=rcvd, sent=None)
elif message["event"] == "auth":
if message["status"] != "OK":
raise InvalidCredentialError("Can't authenticate " + \
"with given API-KEY and API-SECRET.")
raise InvalidCredentialError(
"Can't authenticate with given API-KEY and API-SECRET."
)
self.__event_emitter.emit("authenticated", message)
self._authentication = True
if isinstance(message, list) and \
message[0] == 0 and message[1] != Connection._HEARTBEAT:
if (
isinstance(message, list)
and message[0] == 0
and message[1] != Connection._HEARTBEAT
):
self.__handler.handle(message[1], message[2])
async def __new_bucket(self) -> BfxWebSocketBucket:
bucket = BfxWebSocketBucket( \
self._host, self.__event_emitter)
bucket = BfxWebSocketBucket(self._host, self.__event_emitter)
self.__buckets[bucket] = asyncio \
.create_task(bucket.start())
self.__buckets[bucket] = asyncio.create_task(bucket.start())
await bucket.wait()
return bucket
@Connection._require_websocket_connection
async def subscribe(self,
channel: str,
sub_id: Optional[str] = None,
**kwargs: Any) -> None:
if not channel in ["ticker", "trades", "book", "candles", "status"]:
raise UnknownChannelError("Available channels are: " + \
"ticker, trades, book, candles and status.")
async def subscribe(
self, channel: str, sub_id: Optional[str] = None, **kwargs: Any
) -> None:
if channel not in ["ticker", "trades", "book", "candles", "status"]:
raise UnknownChannelError(
"Available channels are: ticker, trades, book, candles and status."
)
for bucket in self.__buckets:
if sub_id in bucket.ids:
raise SubIdError("sub_id must be " + \
"unique for all subscriptions.")
raise SubIdError("sub_id must be unique for all subscriptions.")
for bucket in self.__buckets:
if not bucket.is_full:
return await bucket.subscribe( \
channel, sub_id, **kwargs)
return await bucket.subscribe(channel, sub_id, **kwargs)
bucket = await self.__new_bucket()
return await bucket.subscribe( \
channel, sub_id, **kwargs)
return await bucket.subscribe(channel, sub_id, **kwargs)
@Connection._require_websocket_connection
async def unsubscribe(self, sub_id: str) -> None:
# pylint: disable-next=consider-using-dict-items
for bucket in self.__buckets:
if bucket.has(sub_id):
if bucket.count == 1:
del self.__buckets[bucket]
return await bucket.close( \
code=1001, reason="Going Away")
return await bucket.close(code=1001, reason="Going Away")
return await bucket.unsubscribe(sub_id)
raise UnknownSubscriptionError("Unable to find " + \
f"a subscription with sub_id <{sub_id}>.")
raise UnknownSubscriptionError(
f"Unable to find a subscription with sub_id <{sub_id}>."
)
@Connection._require_websocket_connection
async def resubscribe(self, sub_id: str) -> None:
@@ -310,31 +321,31 @@ class BfxWebSocketClient(Connection):
if bucket.has(sub_id):
return await bucket.resubscribe(sub_id)
raise UnknownSubscriptionError("Unable to find " + \
f"a subscription with sub_id <{sub_id}>.")
raise UnknownSubscriptionError(
f"Unable to find a subscription with sub_id <{sub_id}>."
)
@Connection._require_websocket_connection
async def close(self, code: int = 1000, reason: str = str()) -> None:
async def close(self, code: int = 1000, reason: str = "") -> None:
for bucket in self.__buckets:
await bucket.close(code=code, reason=reason)
if self._websocket.open:
await self._websocket.close( \
code=code, reason=reason)
await self._websocket.close(code=code, reason=reason)
@Connection._require_websocket_authentication
async def notify(self,
info: Any,
message_id: Optional[int] = None,
**kwargs: Any) -> None:
async def notify(
self, info: Any, message_id: Optional[int] = None, **kwargs: Any
) -> None:
await self._websocket.send(
json.dumps([ 0, "n", message_id,
{ "type": "ucm-test", "info": info, **kwargs } ]))
json.dumps(
[0, "n", message_id, {"type": "ucm-test", "info": info, **kwargs}]
)
)
@Connection._require_websocket_authentication
async def __handle_websocket_input(self, event: str, data: Any) -> None:
await self._websocket.send(json.dumps( \
[ 0, event, None, data], cls=JSONEncoder))
await self._websocket.send(json.dumps([0, event, None, data], cls=JSONEncoder))
def on(self, event, callback = None):
def on(self, event, callback=None):
return self.__event_emitter.on(event, callback)

View File

@@ -1,95 +1,128 @@
from typing import \
Callable, Awaitable, Tuple, \
List, Union, Optional, \
Any
from decimal import Decimal
from typing import Any, Awaitable, Callable, List, Optional, Tuple, Union
_Handler = Callable[[str, Any], Awaitable[None]]
class BfxWebSocketInputs:
def __init__(self, handle_websocket_input: _Handler) -> None:
self.__handle_websocket_input = handle_websocket_input
async def submit_order(self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
price: Union[str, float, Decimal],
*,
lev: Optional[int] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = None,
tif: Optional[str] = None) -> 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
})
async def submit_order(
self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
price: Union[str, float, Decimal],
*,
lev: Optional[int] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = None,
tif: Optional[str] = None,
) -> 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,
},
)
async def update_order(self,
id: int,
*,
amount: Optional[Union[str, float, Decimal]] = None,
price: Optional[Union[str, float, Decimal]] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
gid: Optional[int] = None,
flags: Optional[int] = None,
lev: Optional[int] = None,
delta: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None) -> 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 update_order(
self,
id: int,
*,
amount: Optional[Union[str, float, Decimal]] = None,
price: Optional[Union[str, float, Decimal]] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
gid: Optional[int] = None,
flags: Optional[int] = None,
lev: Optional[int] = None,
delta: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None,
) -> 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) -> None:
await self.__handle_websocket_input("oc", {
"id": id, "cid": cid, "cid_date": cid_date
})
async def cancel_order(
self,
*,
id: Optional[int] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"oc", {"id": id, "cid": cid, "cid_date": cid_date}
)
async def cancel_order_multi(self,
*,
id: Optional[List[int]] = None,
cid: Optional[List[Tuple[int, str]]] = None,
gid: Optional[List[int]] = None,
all: Optional[bool] = None) -> None:
await self.__handle_websocket_input("oc_multi", {
"id": id, "cid": cid, "gid": gid,
"all": all
})
async def cancel_order_multi(
self,
*,
id: Optional[List[int]] = None,
cid: Optional[List[Tuple[int, str]]] = None,
gid: Optional[List[int]] = None,
all: Optional[bool] = None,
) -> None:
await self.__handle_websocket_input(
"oc_multi", {"id": id, "cid": cid, "gid": gid, "all": all}
)
#pylint: disable-next=too-many-arguments
async def submit_funding_offer(self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal],
period: int,
*,
flags: Optional[int] = None) -> None:
await self.__handle_websocket_input("fon", {
"type": type, "symbol": symbol, "amount": amount,
"rate": rate, "period": period, "flags": flags
})
async def submit_funding_offer(
self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal],
period: int,
*,
flags: Optional[int] = None,
) -> None:
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) -> None:
await self.__handle_websocket_input("foc", { "id": id })
await self.__handle_websocket_input("foc", {"id": id})
async def calc(self, *args: str) -> None:
await self.__handle_websocket_input("calc",
list(map(lambda arg: [arg], args)))
await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args)))

View File

@@ -1,25 +1,15 @@
from typing import \
TypeVar, Callable, Awaitable, \
List, Dict, Optional, \
Any, cast
# pylint: disable-next=wrong-import-order
from typing_extensions import \
ParamSpec, Concatenate
from abc import \
ABC, abstractmethod
from functools import wraps
import hashlib
import hmac
import json
from abc import ABC, abstractmethod
from datetime import datetime
from functools import wraps
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, cast
import hmac, hashlib, json
from typing_extensions import Concatenate, ParamSpec
from websockets.client import WebSocketClientProtocol
from bfxapi.websocket.exceptions import \
ConnectionNotOpen, ActionRequiresAuthentication
from bfxapi.websocket.exceptions import ActionRequiresAuthentication, ConnectionNotOpen
_S = TypeVar("_S", bound="Connection")
@@ -27,6 +17,7 @@ _R = TypeVar("_R")
_P = ParamSpec("_P")
class Connection(ABC):
_HEARTBEAT = "hb"
@@ -39,8 +30,7 @@ class Connection(ABC):
@property
def open(self) -> bool:
return self.__protocol is not None and \
self.__protocol.open
return self.__protocol is not None and self.__protocol.open
@property
def authentication(self) -> bool:
@@ -55,12 +45,11 @@ class Connection(ABC):
self.__protocol = protocol
@abstractmethod
async def start(self) -> None:
...
async def start(self) -> None: ...
@staticmethod
def _require_websocket_connection(
function: Callable[Concatenate[_S, _P], Awaitable[_R]]
function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
@@ -73,13 +62,15 @@ class Connection(ABC):
@staticmethod
def _require_websocket_authentication(
function: Callable[Concatenate[_S, _P], Awaitable[_R]]
function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
if not self.authentication:
raise ActionRequiresAuthentication("To perform this action you need to " \
"authenticate using your API_KEY and API_SECRET.")
raise ActionRequiresAuthentication(
"To perform this action you need to "
"authenticate using your API_KEY and API_SECRET."
)
internal = Connection._require_websocket_connection(function)
@@ -89,12 +80,13 @@ class Connection(ABC):
@staticmethod
def _get_authentication_message(
api_key: str,
api_secret: str,
filters: Optional[List[str]] = None
api_key: str, api_secret: str, filters: Optional[List[str]] = None
) -> str:
message: Dict[str, Any] = \
{ "event": "auth", "filter": filters, "apiKey": api_key }
message: Dict[str, Any] = {
"event": "auth",
"filter": filters,
"apiKey": api_key,
}
message["authNonce"] = round(datetime.now().timestamp() * 1_000_000)
@@ -103,7 +95,7 @@ class Connection(ABC):
auth_sig = hmac.new(
key=api_secret.encode("utf8"),
msg=message["authPayload"].encode("utf8"),
digestmod=hashlib.sha384
digestmod=hashlib.sha384,
)
message["authSig"] = auth_sig.hexdigest()

View File

@@ -1,10 +1,7 @@
from typing import \
TypeVar, Callable, List, \
Dict, Union, Optional, \
Any
from collections import defaultdict
from asyncio import AbstractEventLoop
from collections import defaultdict
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
from pyee.asyncio import AsyncIOEventEmitter
from bfxapi.websocket.exceptions import UnknownEventError
@@ -12,57 +9,86 @@ from bfxapi.websocket.exceptions import UnknownEventError
_Handler = TypeVar("_Handler", bound=Callable[..., None])
_ONCE_PER_CONNECTION = [
"open", "authenticated", "order_snapshot",
"position_snapshot", "funding_offer_snapshot", "funding_credit_snapshot",
"funding_loan_snapshot", "wallet_snapshot"
"open",
"authenticated",
"order_snapshot",
"position_snapshot",
"funding_offer_snapshot",
"funding_credit_snapshot",
"funding_loan_snapshot",
"wallet_snapshot",
]
_ONCE_PER_SUBSCRIPTION = [
"subscribed", "t_trades_snapshot", "f_trades_snapshot",
"t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot",
"f_raw_book_snapshot", "candles_snapshot"
"subscribed",
"t_trades_snapshot",
"f_trades_snapshot",
"t_book_snapshot",
"f_book_snapshot",
"t_raw_book_snapshot",
"f_raw_book_snapshot",
"candles_snapshot",
]
_COMMON = [
"disconnected", "t_ticker_update", "f_ticker_update",
"t_trade_execution", "t_trade_execution_update", "f_trade_execution",
"f_trade_execution_update", "t_book_update", "f_book_update",
"t_raw_book_update", "f_raw_book_update", "candles_update",
"derivatives_status_update", "liquidation_feed_update", "checksum",
"order_new", "order_update", "order_cancel",
"position_new", "position_update", "position_close",
"funding_offer_new", "funding_offer_update", "funding_offer_cancel",
"funding_credit_new", "funding_credit_update", "funding_credit_close",
"funding_loan_new", "funding_loan_update", "funding_loan_close",
"trade_execution", "trade_execution_update", "wallet_update",
"notification", "on-req-notification", "ou-req-notification",
"oc-req-notification", "fon-req-notification", "foc-req-notification"
"disconnected",
"t_ticker_update",
"f_ticker_update",
"t_trade_execution",
"t_trade_execution_update",
"f_trade_execution",
"f_trade_execution_update",
"t_book_update",
"f_book_update",
"t_raw_book_update",
"f_raw_book_update",
"candles_update",
"derivatives_status_update",
"liquidation_feed_update",
"checksum",
"order_new",
"order_update",
"order_cancel",
"position_new",
"position_update",
"position_close",
"funding_offer_new",
"funding_offer_update",
"funding_offer_cancel",
"funding_credit_new",
"funding_credit_update",
"funding_credit_close",
"funding_loan_new",
"funding_loan_update",
"funding_loan_close",
"trade_execution",
"trade_execution_update",
"wallet_update",
"notification",
"on-req-notification",
"ou-req-notification",
"oc-req-notification",
"fon-req-notification",
"foc-req-notification",
]
class BfxEventEmitter(AsyncIOEventEmitter):
_EVENTS = _ONCE_PER_CONNECTION + \
_ONCE_PER_SUBSCRIPTION + \
_COMMON
_EVENTS = _ONCE_PER_CONNECTION + _ONCE_PER_SUBSCRIPTION + _COMMON
def __init__(self, loop: Optional[AbstractEventLoop] = None) -> None:
super().__init__(loop)
self._connection: List[str] = [ ]
self._connection: List[str] = []
self._subscriptions: Dict[str, List[str]] = \
defaultdict(lambda: [ ])
self._subscriptions: Dict[str, List[str]] = defaultdict(lambda: [])
def emit(
self,
event: str,
*args: Any,
**kwargs: Any
) -> bool:
def emit(self, event: str, *args: Any, **kwargs: Any) -> bool:
if event in _ONCE_PER_CONNECTION:
if event in self._connection:
return self._has_listeners(event)
self._connection += [ event ]
self._connection += [event]
if event in _ONCE_PER_SUBSCRIPTION:
sub_id = args[0]["sub_id"]
@@ -70,7 +96,7 @@ class BfxEventEmitter(AsyncIOEventEmitter):
if event in self._subscriptions[sub_id]:
return self._has_listeners(event)
self._subscriptions[sub_id] += [ event ]
self._subscriptions[sub_id] += [event]
return super().emit(event, *args, **kwargs)
@@ -78,8 +104,10 @@ class BfxEventEmitter(AsyncIOEventEmitter):
self, event: str, f: Optional[_Handler] = None
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
if event not in BfxEventEmitter._EVENTS:
raise UnknownEventError(f"Can't register to unknown event: <{event}> " + \
"(to get a full list of available events see https://docs.bitfinex.com/).")
raise UnknownEventError(
f"Can't register to unknown event: <{event}> (to get a full"
"list of available events see https://docs.bitfinex.com/)."
)
return super().on(event, f)

View File

@@ -1,3 +1,2 @@
from .public_channels_handler import PublicChannelsHandler
from .auth_events_handler import AuthEventsHandler
from .public_channels_handler import PublicChannelsHandler

View File

@@ -1,25 +1,38 @@
from typing import \
Dict, Tuple, Any
from typing import Any, Dict, Tuple
from pyee.base import EventEmitter
from bfxapi.types import serializers
from bfxapi.types.dataclasses import FundingOffer, Order
from bfxapi.types.serializers import _Notification
from bfxapi.types.dataclasses import \
Order, FundingOffer
class AuthEventsHandler:
__ABBREVIATIONS = {
"os": "order_snapshot", "on": "order_new", "ou": "order_update",
"oc": "order_cancel", "ps": "position_snapshot", "pn": "position_new",
"pu": "position_update", "pc": "position_close", "te": "trade_execution",
"tu": "trade_execution_update", "fos": "funding_offer_snapshot", "fon": "funding_offer_new",
"fou": "funding_offer_update", "foc": "funding_offer_cancel", "fcs": "funding_credit_snapshot",
"fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close",
"fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update",
"flc": "funding_loan_close", "ws": "wallet_snapshot", "wu": "wallet_update"
"os": "order_snapshot",
"on": "order_new",
"ou": "order_update",
"oc": "order_cancel",
"ps": "position_snapshot",
"pn": "position_new",
"pu": "position_update",
"pc": "position_close",
"te": "trade_execution",
"tu": "trade_execution_update",
"fos": "funding_offer_snapshot",
"fon": "funding_offer_new",
"fou": "funding_offer_update",
"foc": "funding_offer_cancel",
"fcs": "funding_credit_snapshot",
"fcn": "funding_credit_new",
"fcu": "funding_credit_update",
"fcc": "funding_credit_close",
"fls": "funding_loan_snapshot",
"fln": "funding_loan_new",
"flu": "funding_loan_update",
"flc": "funding_loan_close",
"ws": "wallet_snapshot",
"wu": "wallet_update",
}
__SERIALIZERS: Dict[Tuple[str, ...], serializers._Serializer] = {
@@ -29,7 +42,7 @@ class AuthEventsHandler:
("fos", "fon", "fou", "foc"): serializers.FundingOffer,
("fcs", "fcn", "fcu", "fcc"): serializers.FundingCredit,
("fls", "fln", "flu", "flc"): serializers.FundingLoan,
("ws", "wu"): serializers.Wallet
("ws", "wu"): serializers.Wallet,
}
def __init__(self, event_emitter: EventEmitter) -> None:
@@ -44,7 +57,7 @@ class AuthEventsHandler:
event = AuthEventsHandler.__ABBREVIATIONS[abbrevation]
if all(isinstance(sub_stream, list) for sub_stream in stream):
data = [ serializer.parse(*sub_stream) for sub_stream in stream ]
data = [serializer.parse(*sub_stream) for sub_stream in stream]
else:
data = serializer.parse(*stream)
@@ -56,11 +69,13 @@ class AuthEventsHandler:
serializer: _Notification = _Notification[None](serializer=None)
if stream[1] in ("on-req", "ou-req", "oc-req"):
event, serializer = f"{stream[1]}-notification", \
_Notification[Order](serializer=serializers.Order)
event, serializer = f"{stream[1]}-notification", _Notification[Order](
serializer=serializers.Order
)
if stream[1] in ("fon-req", "foc-req"):
event, serializer = f"{stream[1]}-notification", \
_Notification[FundingOffer](serializer=serializers.FundingOffer)
event, serializer = f"{stream[1]}-notification", _Notification[
FundingOffer
](serializer=serializers.FundingOffer)
self.__event_emitter.emit(event, serializer.parse(*stream))

View File

@@ -1,16 +1,20 @@
from typing import \
List, Any, cast
from typing import Any, List, cast
from pyee.base import EventEmitter
from bfxapi.types import serializers
from bfxapi.websocket.subscriptions import \
Subscription, Ticker, Trades, \
Book, Candles, Status
from bfxapi.websocket.subscriptions import (
Book,
Candles,
Status,
Subscription,
Ticker,
Trades,
)
_CHECKSUM = "cs"
class PublicChannelsHandler:
def __init__(self, event_emitter: EventEmitter) -> None:
self.__event_emitter = event_emitter
@@ -35,101 +39,160 @@ class PublicChannelsHandler:
elif subscription["channel"] == "status":
self.__status_channel_handler(cast(Status, subscription), stream)
#pylint: disable-next=inconsistent-return-statements
def __ticker_channel_handler(self, subscription: Ticker, stream: List[Any]):
if subscription["symbol"].startswith("t"):
return self.__event_emitter.emit("t_ticker_update", subscription, \
serializers.TradingPairTicker.parse(*stream[0]))
return self.__event_emitter.emit(
"t_ticker_update",
subscription,
serializers.TradingPairTicker.parse(*stream[0]),
)
if subscription["symbol"].startswith("f"):
return self.__event_emitter.emit("f_ticker_update", subscription, \
serializers.FundingCurrencyTicker.parse(*stream[0]))
return self.__event_emitter.emit(
"f_ticker_update",
subscription,
serializers.FundingCurrencyTicker.parse(*stream[0]),
)
#pylint: disable-next=inconsistent-return-statements
def __trades_channel_handler(self, subscription: Trades, stream: List[Any]):
if (event := stream[0]) and event in [ "te", "tu", "fte", "ftu" ]:
events = { "te": "t_trade_execution", "tu": "t_trade_execution_update", \
"fte": "f_trade_execution", "ftu": "f_trade_execution_update" }
if (event := stream[0]) and event in ["te", "tu", "fte", "ftu"]:
events = {
"te": "t_trade_execution",
"tu": "t_trade_execution_update",
"fte": "f_trade_execution",
"ftu": "f_trade_execution_update",
}
if subscription["symbol"].startswith("t"):
return self.__event_emitter.emit(events[event], subscription, \
serializers.TradingPairTrade.parse(*stream[1]))
return self.__event_emitter.emit(
events[event],
subscription,
serializers.TradingPairTrade.parse(*stream[1]),
)
if subscription["symbol"].startswith("f"):
return self.__event_emitter.emit(events[event], subscription, \
serializers.FundingCurrencyTrade.parse(*stream[1]))
return self.__event_emitter.emit(
events[event],
subscription,
serializers.FundingCurrencyTrade.parse(*stream[1]),
)
if subscription["symbol"].startswith("t"):
return self.__event_emitter.emit("t_trades_snapshot", subscription, \
[ serializers.TradingPairTrade.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"t_trades_snapshot",
subscription,
[
serializers.TradingPairTrade.parse(*sub_stream)
for sub_stream in stream[0]
],
)
if subscription["symbol"].startswith("f"):
return self.__event_emitter.emit("f_trades_snapshot", subscription, \
[ serializers.FundingCurrencyTrade.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"f_trades_snapshot",
subscription,
[
serializers.FundingCurrencyTrade.parse(*sub_stream)
for sub_stream in stream[0]
],
)
#pylint: disable-next=inconsistent-return-statements
def __book_channel_handler(self, subscription: Book, stream: List[Any]):
if subscription["symbol"].startswith("t"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("t_book_snapshot", subscription, \
[ serializers.TradingPairBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"t_book_snapshot",
subscription,
[
serializers.TradingPairBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("t_book_update", subscription, \
serializers.TradingPairBook.parse(*stream[0]))
return self.__event_emitter.emit(
"t_book_update",
subscription,
serializers.TradingPairBook.parse(*stream[0]),
)
if subscription["symbol"].startswith("f"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("f_book_snapshot", subscription, \
[ serializers.FundingCurrencyBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"f_book_snapshot",
subscription,
[
serializers.FundingCurrencyBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("f_book_update", subscription, \
serializers.FundingCurrencyBook.parse(*stream[0]))
return self.__event_emitter.emit(
"f_book_update",
subscription,
serializers.FundingCurrencyBook.parse(*stream[0]),
)
#pylint: disable-next=inconsistent-return-statements
def __raw_book_channel_handler(self, subscription: Book, stream: List[Any]):
if subscription["symbol"].startswith("t"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("t_raw_book_snapshot", subscription, \
[ serializers.TradingPairRawBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"t_raw_book_snapshot",
subscription,
[
serializers.TradingPairRawBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("t_raw_book_update", subscription, \
serializers.TradingPairRawBook.parse(*stream[0]))
return self.__event_emitter.emit(
"t_raw_book_update",
subscription,
serializers.TradingPairRawBook.parse(*stream[0]),
)
if subscription["symbol"].startswith("f"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("f_raw_book_snapshot", subscription, \
[ serializers.FundingCurrencyRawBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"f_raw_book_snapshot",
subscription,
[
serializers.FundingCurrencyRawBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("f_raw_book_update", subscription, \
serializers.FundingCurrencyRawBook.parse(*stream[0]))
return self.__event_emitter.emit(
"f_raw_book_update",
subscription,
serializers.FundingCurrencyRawBook.parse(*stream[0]),
)
#pylint: disable-next=inconsistent-return-statements
def __candles_channel_handler(self, subscription: Candles, stream: List[Any]):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("candles_snapshot", subscription, \
[ serializers.Candle.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"candles_snapshot",
subscription,
[serializers.Candle.parse(*sub_stream) for sub_stream in stream[0]],
)
return self.__event_emitter.emit("candles_update", subscription, \
serializers.Candle.parse(*stream[0]))
return self.__event_emitter.emit(
"candles_update", subscription, serializers.Candle.parse(*stream[0])
)
#pylint: disable-next=inconsistent-return-statements
def __status_channel_handler(self, subscription: Status, stream: List[Any]):
if subscription["key"].startswith("deriv:"):
return self.__event_emitter.emit("derivatives_status_update", subscription, \
serializers.DerivativesStatus.parse(*stream[0]))
return self.__event_emitter.emit(
"derivatives_status_update",
subscription,
serializers.DerivativesStatus.parse(*stream[0]),
)
if subscription["key"].startswith("liq:"):
return self.__event_emitter.emit("liquidation_feed_update", subscription, \
serializers.Liquidation.parse(*stream[0][0]))
return self.__event_emitter.emit(
"liquidation_feed_update",
subscription,
serializers.Liquidation.parse(*stream[0][0]),
)
#pylint: disable-next=inconsistent-return-statements
def __checksum_handler(self, subscription: Book, value: int):
return self.__event_emitter.emit( \
"checksum", subscription, value & 0xFFFFFFFF)
return self.__event_emitter.emit("checksum", subscription, value & 0xFFFFFFFF)

View File

@@ -1,25 +1,33 @@
from bfxapi.exceptions import BfxBaseException
class ConnectionNotOpen(BfxBaseException):
pass
class ActionRequiresAuthentication(BfxBaseException):
pass
class ReconnectionTimeoutError(BfxBaseException):
pass
class VersionMismatchError(BfxBaseException):
pass
class SubIdError(BfxBaseException):
pass
class UnknownChannelError(BfxBaseException):
pass
class UnknownEventError(BfxBaseException):
pass
class UnknownSubscriptionError(BfxBaseException):
pass

View File

@@ -1,20 +1,22 @@
from typing import \
Union, Literal, TypedDict
from typing import Literal, TypedDict, Union
Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"]
Channel = Literal["ticker", "trades", "book", "candles", "status"]
class Ticker(TypedDict):
channel: Literal["ticker"]
sub_id: str
symbol: str
class Trades(TypedDict):
channel: Literal["trades"]
sub_id: str
symbol: str
class Book(TypedDict):
channel: Literal["book"]
sub_id: str
@@ -23,11 +25,13 @@ class Book(TypedDict):
freq: Literal["F0", "F1"]
len: Literal["1", "25", "100", "250"]
class Candles(TypedDict):
channel: Literal["candles"]
sub_id: str
key: str
class Status(TypedDict):
channel: Literal["status"]
sub_id: str

Binary file not shown.

View File

@@ -5,10 +5,7 @@ import os
from bfxapi import Client
from bfxapi.types import Notification, PositionClaim
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
# Claims all active positions
for position in bfx.rest.auth.get_positions():

View File

@@ -13,10 +13,7 @@ from bfxapi.types import (
Withdrawal,
)
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
# Gets all user's available wallets
wallets: List[Wallet] = bfx.rest.auth.get_wallets()

View File

@@ -8,10 +8,7 @@ from bfxapi.types import (
DerivativePositionCollateralLimits,
)
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
submit_order_notification = bfx.rest.auth.submit_order(
type="LIMIT", symbol="tBTCF0:USTF0", amount="0.015", price="16700", lev=10

View File

@@ -5,10 +5,7 @@ import os
from bfxapi import Client
from bfxapi.types import FundingOffer, Notification
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
# Submit a new funding offer
notification: Notification[FundingOffer] = bfx.rest.auth.submit_funding_offer(

View File

@@ -5,10 +5,7 @@ import os
from bfxapi import Client
from bfxapi.types import Notification, Order
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
# Submit a new order
submit_order_notification: Notification[Order] = bfx.rest.auth.submit_order(

View File

@@ -6,10 +6,7 @@ from typing import List
from bfxapi import Client
from bfxapi.types import FundingLoan, Notification
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
loans: List[FundingLoan] = bfx.rest.auth.get_funding_loans(symbol="fUSD")

View File

@@ -4,10 +4,7 @@ import os
from bfxapi import Client
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
if not bfx.rest.merchant.set_merchant_settings("bfx_pay_recommend_store", 1):
print("Cannot set <bfx_pay_recommend_store> to <1>.")

View File

@@ -5,10 +5,7 @@ import os
from bfxapi import Client
from bfxapi.types import InvoiceSubmission
bfx = Client(
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
customer_info = {
"nationality": "DE",

2
pyproject.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
target-version = ["py38", "py39", "py310", "py311"]

View File

@@ -1,16 +1,15 @@
from distutils.core import setup
_version = { }
with open("bfxapi/_version.py", encoding="utf-8") as f:
#pylint: disable-next=exec-used
exec(f.read(), _version)
from bfxapi._version import __version__
setup(
name="bitfinex-api-py",
version=_version["__version__"],
version=__version__,
description="Official Bitfinex Python API",
long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction",
long_description=(
"A Python reference implementation of the Bitfinex API "
"for both REST and websocket interaction."
),
long_description_content_type="text/markdown",
url="https://github.com/bitfinexcom/bitfinex-api-py",
author="Bitfinex",
@@ -18,12 +17,9 @@ setup(
license="Apache-2.0",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
@@ -52,5 +48,5 @@ setup(
"requests~=2.28.1",
"urllib3~=1.26.14",
],
python_requires=">=3.8"
)
python_requires=">=3.8",
)