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) ## Steps to reproduce (for bugs)
<!-- You can delete this section if you are not submitting a bug report --> <!-- You can delete this section if you are not submitting a bug report -->
1. &nbsp; 1.
2. &nbsp; 2.
3. &nbsp; 3.
### Python version ### Python version
<!-- Indicate your python version here --> <!-- Indicate your python version here -->

View File

@@ -20,10 +20,11 @@ PR fixes the following issue:
# Checklist: # Checklist:
- [ ] My code follows the style guidelines of this project; - [ ] I've done a self-review of my code;
- [ ] I have performed a self-review of my code; - [ ] I've made corresponding changes to the documentation;
- [ ] I have commented my code, particularly in hard-to-understand areas; - [ ] I've made sure my changes generate no warnings;
- [ ] I have made corresponding changes to the documentation; - [ ] mypy returns no errors when run on the root package;
- [ ] My changes generate no new warnings; <!-- If you use pre-commit hooks you can always check off the following tasks -->
- [ ] Mypy returns no errors or warnings when run on the root package; - [ ] I've run black to format my code;
- [ ] Pylint returns a score of 10.00/10.00 when run on the root package; - [ ] 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: branches:
- master - master
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -23,7 +20,7 @@ jobs:
python-version: '3.8' python-version: '3.8'
- name: Install bitfinex-api-py's dependencies - name: Install bitfinex-api-py's dependencies
run: python -m pip install -r dev-requirements.txt 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) - name: Run pre-commit hooks (see .pre-commit-config.yaml)
run: python -m pylint bfxapi uses: pre-commit/action@v3.0.1
- name: Run mypy to check the correctness of type hinting (and fail if any error or warning is found) - name: Run mypy to ensure correct type hinting
run: python -m mypy bfxapi run: python -m mypy bfxapi

22
.gitignore vendored
View File

@@ -1,12 +1,16 @@
.venv
.DS_Store
.vscode .vscode
*.pyc .python-version
*.log
bitfinex_api_py.egg-info
__pycache__ __pycache__
dist bitfinex_api_py.egg-info
venv bitfinex_api_py.dist-info
!.gitkeep build/
MANIFEST 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) 1. [Installation and setup](#installation-and-setup)
* [Cloning the repository](#cloning-the-repository) * [Cloning the repository](#cloning-the-repository)
* [Installing the dependencies](#installing-the-dependencies) * [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) 2. [Before opening a PR](#before-opening-a-pr)
* [Tip](#tip)
3. [License](#license) 3. [License](#license)
## Installation and setup ## 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 python3 -m pip install -r dev-requirements.txt
``` ```
Make sure to install `dev-requirements.txt` instead of `requirements.txt`. \ Make sure to install `dev-requirements.txt` (and not `requirements.txt`!). \
`dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependencies. \ `dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependency. \
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. 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. 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 ## 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. 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: ### Tip
- [ ] [`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). Setting up the project's pre-commit hooks will help automate this process ([more](#set-up-the-pre-commit-hooks-optional)).
## License ## License

View File

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

View File

@@ -1,15 +1,12 @@
from typing import \ from typing import TYPE_CHECKING, List, Optional
TYPE_CHECKING, List, Optional
from bfxapi._utils.logging import ColorLogger from bfxapi._utils.logging import ColorLogger
from bfxapi.exceptions import IncompleteCredentialError
from bfxapi.rest import BfxRestInterface from bfxapi.rest import BfxRestInterface
from bfxapi.websocket import BfxWebSocketClient from bfxapi.websocket import BfxWebSocketClient
from bfxapi.exceptions import IncompleteCredentialError
if TYPE_CHECKING: if TYPE_CHECKING:
from bfxapi.websocket._client.bfx_websocket_client import \ from bfxapi.websocket._client.bfx_websocket_client import _Credentials
_Credentials
REST_HOST = "https://api.bitfinex.com/v2" REST_HOST = "https://api.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2" 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_REST_HOST = "https://api-pub.bitfinex.com/v2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
class Client: class Client:
def __init__( def __init__(
self, self,
api_key: Optional[str] = None, api_key: Optional[str] = None,
api_secret: Optional[str] = None, api_secret: Optional[str] = None,
*, *,
rest_host: str = REST_HOST, rest_host: str = REST_HOST,
wss_host: str = WSS_HOST, wss_host: str = WSS_HOST,
filters: Optional[List[str]] = None, filters: Optional[List[str]] = None,
timeout: Optional[int] = 60 * 15, timeout: Optional[int] = 60 * 15,
log_filename: Optional[str] = None log_filename: Optional[str] = None,
) -> None: ) -> None:
credentials: Optional["_Credentials"] = None credentials: Optional["_Credentials"] = None
if api_key and api_secret: if api_key and api_secret:
credentials = \ credentials = {
{ "api_key": api_key, "api_secret": api_secret, "filters": filters } "api_key": api_key,
"api_secret": api_secret,
"filters": filters,
}
elif api_key: elif api_key:
raise IncompleteCredentialError( \ raise IncompleteCredentialError(
"You must provide both an API-KEY and an API-SECRET (missing API-KEY).") "You must provide both API-KEY and API-SECRET (missing API-KEY)."
)
elif api_secret: elif api_secret:
raise IncompleteCredentialError( \ raise IncompleteCredentialError(
"You must provide both an API-KEY and an API-SECRET (missing API-SECRET).") "You must provide both API-KEY and API-SECRET (missing API-SECRET)."
)
self.rest = BfxRestInterface(rest_host, api_key, api_secret) self.rest = BfxRestInterface(rest_host, api_key, api_secret)
@@ -48,5 +51,6 @@ class Client:
if log_filename: if log_filename:
logger.register(filename=log_filename) logger.register(filename=log_filename)
self.wss = BfxWebSocketClient(wss_host, \ self.wss = BfxWebSocketClient(
credentials=credentials, timeout=timeout, logger=logger) 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: def _to_snake_case(string: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", string).lower() return re.sub(r"(?<!^)(?=[A-Z])", "_", string).lower()
def _object_hook(data: Dict[str, Any]) -> Any: 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): class JSONDecoder(json.JSONDecoder):
def __init__(self, *args: Any, **kwargs: Any) -> None: 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 import json
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Union
_ExtJSON = Union[Dict[str, "_ExtJSON"], List["_ExtJSON"], \ _ExtJSON = Union[
bool, int, float, str, Decimal, None] 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]: def _clear(dictionary: Dict[str, Any]) -> Dict[str, Any]:
return { key: value for key, value in dictionary.items() \ return {key: value for key, value in dictionary.items() if value is not None}
if value is not None }
def _adapter(data: _ExtJSON) -> _StrictJSON: def _adapter(data: _ExtJSON) -> _StrictJSON:
if isinstance(data, bool): if isinstance(data, bool):
@@ -25,12 +22,13 @@ def _adapter(data: _ExtJSON) -> _StrictJSON:
return format(data, "f") return format(data, "f")
if isinstance(data, list): 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): 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 return data
class JSONEncoder(json.JSONEncoder): class JSONEncoder(json.JSONEncoder):
def encode(self, o: _ExtJSON) -> str: def encode(self, o: _ExtJSON) -> str:
return super().encode(_adapter(o)) 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 import sys
from copy import copy
from logging import FileHandler, Formatter, Logger, LogRecord, StreamHandler
from typing import TYPE_CHECKING, Literal, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
_Level = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] _Level = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
_BLACK, _RED, _GREEN, _YELLOW, \ _BLACK, _RED, _GREEN, _YELLOW, _BLUE, _MAGENTA, _CYAN, _WHITE = [
_BLUE, _MAGENTA, _CYAN, _WHITE = \ f"\033[0;{90 + i}m" for i in range(8)
[ 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 = \ _BOLD_BLACK,
[ f"\033[1;{90 + i}m" for i in range(8) ] _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" _NC = "\033[0m"
class _ColorFormatter(Formatter): class _ColorFormatter(Formatter):
__LEVELS = { __LEVELS = {
"INFO": _BLUE, "INFO": _BLUE,
"WARNING": _YELLOW, "WARNING": _YELLOW,
"ERROR": _RED, "ERROR": _RED,
"CRITICAL": _BOLD_RED, "CRITICAL": _BOLD_RED,
"DEBUG": _BOLD_WHITE "DEBUG": _BOLD_WHITE,
} }
def format(self, record: LogRecord) -> str: def format(self, record: LogRecord) -> str:
@@ -37,7 +40,6 @@ class _ColorFormatter(Formatter):
return super().format(_record) return super().format(_record)
#pylint: disable-next=invalid-name
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str: def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
return _GREEN + super().formatTime(record, datefmt) + _NC return _GREEN + super().formatTime(record, datefmt) + _NC
@@ -45,12 +47,14 @@ class _ColorFormatter(Formatter):
def __format_level(level: str) -> str: def __format_level(level: str) -> str:
return _ColorFormatter.__LEVELS[level] + level + _NC return _ColorFormatter.__LEVELS[level] + level + _NC
_FORMAT = "%(asctime)s %(name)s %(levelname)s %(message)s" _FORMAT = "%(asctime)s %(name)s %(levelname)s %(message)s"
_DATE_FORMAT = "%d-%m-%Y %H:%M:%S" _DATE_FORMAT = "%d-%m-%Y %H:%M:%S"
class ColorLogger(Logger): class ColorLogger(Logger):
__FORMATTER = Formatter(_FORMAT,_DATE_FORMAT) __FORMATTER = Formatter(_FORMAT, _DATE_FORMAT)
def __init__(self, name: str, level: "_Level" = "NOTSET") -> None: def __init__(self, name: str, level: "_Level" = "NOTSET") -> None:
super().__init__(name, level) super().__init__(name, level)

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
from .bfx_rest_interface import BfxRestInterface from .bfx_rest_interface import BfxRestInterface
from .rest_public_endpoints import RestPublicEndpoints
from .rest_auth_endpoints import RestAuthEndpoints from .rest_auth_endpoints import RestAuthEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints 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_auth_endpoints import RestAuthEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints from .rest_merchant_endpoints import RestMerchantEndpoints
from .rest_public_endpoints import RestPublicEndpoints
class BfxRestInterface: class BfxRestInterface:
VERSION = 2 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.public = RestPublicEndpoints(host=host)
self.auth = RestAuthEndpoints(host=host, api_key=api_key, api_secret=api_secret) 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 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 ..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): class RestAuthEndpoints(Middleware):
def get_user_info(self) -> UserInfo: def get_user_info(self) -> UserInfo:
return serializers.UserInfo \ return serializers.UserInfo.parse(*self._post("auth/r/info/user"))
.parse(*self._post("auth/r/info/user"))
def get_login_history(self) -> List[LoginHistory]: def get_login_history(self) -> List[LoginHistory]:
return [ serializers.LoginHistory.parse(*sub_data) return [
for sub_data in self._post("auth/r/logins/hist") ] serializers.LoginHistory.parse(*sub_data)
for sub_data in self._post("auth/r/logins/hist")
]
def get_balance_available_for_orders_or_offers(self, def get_balance_available_for_orders_or_offers(
symbol: str, self,
type: str, symbol: str,
*, type: str,
dir: Optional[int] = None, *,
rate: Optional[str] = None, dir: Optional[int] = None,
lev: Optional[str] = None) -> BalanceAvailable: rate: Optional[str] = None,
body = { lev: Optional[str] = None,
"symbol": symbol, "type": type, "dir": dir, ) -> BalanceAvailable:
"rate": rate, "lev": lev body = {"symbol": symbol, "type": type, "dir": dir, "rate": rate, "lev": lev}
}
return serializers.BalanceAvailable \ return serializers.BalanceAvailable.parse(
.parse(*self._post("auth/calc/order/avail", body=body)) *self._post("auth/calc/order/avail", body=body)
)
def get_wallets(self) -> List[Wallet]: def get_wallets(self) -> List[Wallet]:
return [ serializers.Wallet.parse(*sub_data) \ return [
for sub_data in self._post("auth/r/wallets") ] serializers.Wallet.parse(*sub_data)
for sub_data in self._post("auth/r/wallets")
]
def get_orders(self, def get_orders(
*, self, *, symbol: Optional[str] = None, ids: Optional[List[str]] = None
symbol: Optional[str] = None, ) -> List[Order]:
ids: Optional[List[str]] = None) -> List[Order]:
if symbol is None: if symbol is None:
endpoint = "auth/r/orders" endpoint = "auth/r/orders"
else: endpoint = f"auth/r/orders/{symbol}" else:
endpoint = f"auth/r/orders/{symbol}"
return [ serializers.Order.parse(*sub_data) \ return [
for sub_data in self._post(endpoint, body={ "id": ids }) ] serializers.Order.parse(*sub_data)
for sub_data in self._post(endpoint, body={"id": ids})
]
def submit_order(self, def submit_order(
type: str, self,
symbol: str, type: str,
amount: Union[str, float, Decimal], symbol: str,
price: Union[str, float, Decimal], amount: Union[str, float, Decimal],
*, price: Union[str, float, Decimal],
lev: Optional[int] = None, *,
price_trailing: Optional[Union[str, float, Decimal]] = None, lev: Optional[int] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None, price_trailing: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None, price_aux_limit: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None, price_oco_stop: Optional[Union[str, float, Decimal]] = None,
cid: Optional[int] = None, gid: Optional[int] = None,
flags: Optional[int] = None, cid: Optional[int] = None,
tif: Optional[str] = None) -> Notification[Order]: flags: Optional[int] = None,
tif: Optional[str] = None,
) -> Notification[Order]:
body = { body = {
"type": type, "symbol": symbol, "amount": amount, "type": type,
"price": price, "lev": lev, "price_trailing": price_trailing, "symbol": symbol,
"price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid, "amount": amount,
"cid": cid, "flags": flags, "tif": tif "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) \ return _Notification[Order](serializers.Order).parse(
.parse(*self._post("auth/w/order/submit", body=body)) *self._post("auth/w/order/submit", body=body)
)
def update_order(self, def update_order(
id: int, self,
*, id: int,
amount: Optional[Union[str, float, Decimal]] = None, *,
price: Optional[Union[str, float, Decimal]] = None, amount: Optional[Union[str, float, Decimal]] = None,
cid: Optional[int] = None, price: Optional[Union[str, float, Decimal]] = None,
cid_date: Optional[str] = None, cid: Optional[int] = None,
gid: Optional[int] = None, cid_date: Optional[str] = None,
flags: Optional[int] = None, gid: Optional[int] = None,
lev: Optional[int] = None, flags: Optional[int] = None,
delta: Optional[Union[str, float, Decimal]] = None, lev: Optional[int] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None, delta: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None, price_aux_limit: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None) -> Notification[Order]: price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None,
) -> Notification[Order]:
body = { body = {
"id": id, "amount": amount, "price": price, "id": id,
"cid": cid, "cid_date": cid_date, "gid": gid, "amount": amount,
"flags": flags, "lev": lev, "delta": delta, "price": price,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif "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) \ return _Notification[Order](serializers.Order).parse(
.parse(*self._post("auth/w/order/update", body=body)) *self._post("auth/w/order/update", body=body)
)
def cancel_order(self, def cancel_order(
*, self,
id: Optional[int] = None, *,
cid: Optional[int] = None, id: Optional[int] = None,
cid_date: Optional[str] = None) -> Notification[Order]: cid: Optional[int] = None,
return _Notification[Order](serializers.Order) \ cid_date: Optional[str] = None,
.parse(*self._post("auth/w/order/cancel", \ ) -> Notification[Order]:
body={ "id": id, "cid": cid, "cid_date": cid_date })) 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, def cancel_order_multi(
*, self,
id: Optional[List[int]] = None, *,
cid: Optional[List[Tuple[int, str]]] = None, id: Optional[List[int]] = None,
gid: Optional[List[int]] = None, cid: Optional[List[Tuple[int, str]]] = None,
all: Optional[bool] = None) -> Notification[List[Order]]: gid: Optional[List[int]] = None,
body = { all: Optional[bool] = None,
"id": id, "cid": cid, "gid": gid, ) -> Notification[List[Order]]:
"all": all body = {"id": id, "cid": cid, "gid": gid, "all": all}
}
return _Notification[List[Order]](serializers.Order, is_iterable=True) \ return _Notification[List[Order]](serializers.Order, is_iterable=True).parse(
.parse(*self._post("auth/w/order/cancel/multi", body=body)) *self._post("auth/w/order/cancel/multi", body=body)
)
def get_orders_history(self, def get_orders_history(
*, self,
symbol: Optional[str] = None, *,
ids: Optional[List[int]] = None, symbol: Optional[str] = None,
start: Optional[str] = None, ids: Optional[List[int]] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Order]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[Order]:
if symbol is None: if symbol is None:
endpoint = "auth/r/orders/hist" endpoint = "auth/r/orders/hist"
else: endpoint = f"auth/r/orders/{symbol}/hist" else:
endpoint = f"auth/r/orders/{symbol}/hist"
body = { body = {"id": ids, "start": start, "end": end, "limit": limit}
"id": ids, "start": start, "end": end,
"limit": limit
}
return [ serializers.Order.parse(*sub_data) \ return [
for sub_data in self._post(endpoint, body=body) ] serializers.Order.parse(*sub_data)
for sub_data in self._post(endpoint, body=body)
]
def get_order_trades(self, def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]:
symbol: str, return [
id: int) -> List[OrderTrade]: serializers.OrderTrade.parse(*sub_data)
return [ serializers.OrderTrade.parse(*sub_data) \ for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades")
for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades") ] ]
def get_trades_history(self, def get_trades_history(
*, self,
symbol: Optional[str] = None, *,
sort: Optional[int] = None, symbol: Optional[str] = None,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Trade]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[Trade]:
if symbol is None: if symbol is None:
endpoint = "auth/r/trades/hist" endpoint = "auth/r/trades/hist"
else: endpoint = f"auth/r/trades/{symbol}/hist" else:
endpoint = f"auth/r/trades/{symbol}/hist"
body = { body = {"sort": sort, "start": start, "end": end, "limit": limit}
"sort": sort, "start": start, "end": end,
"limit": limit
}
return [ serializers.Trade.parse(*sub_data) \ return [
for sub_data in self._post(endpoint, body=body) ] serializers.Trade.parse(*sub_data)
for sub_data in self._post(endpoint, body=body)
]
def get_ledgers(self, def get_ledgers(
currency: str, self,
*, currency: str,
category: Optional[int] = None, *,
start: Optional[str] = None, category: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Ledger]: end: Optional[str] = None,
body = { limit: Optional[int] = None,
"category": category, "start": start, "end": end, ) -> List[Ledger]:
"limit": limit body = {"category": category, "start": start, "end": end, "limit": limit}
}
return [ serializers.Ledger.parse(*sub_data) \ return [
for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) ] 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: def get_base_margin_info(self) -> BaseMarginInfo:
return serializers.BaseMarginInfo \ return serializers.BaseMarginInfo.parse(
.parse(*(self._post("auth/r/info/margin/base")[1])) *(self._post("auth/r/info/margin/base")[1])
)
def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo:
return serializers.SymbolMarginInfo \ return serializers.SymbolMarginInfo.parse(
.parse(*self._post(f"auth/r/info/margin/{symbol}")) *self._post(f"auth/r/info/margin/{symbol}")
)
def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]:
return [ serializers.SymbolMarginInfo.parse(*sub_data) \ return [
for sub_data in self._post("auth/r/info/margin/sym_all") ] serializers.SymbolMarginInfo.parse(*sub_data)
for sub_data in self._post("auth/r/info/margin/sym_all")
]
def get_positions(self) -> List[Position]: def get_positions(self) -> List[Position]:
return [ serializers.Position.parse(*sub_data) \ return [
for sub_data in self._post("auth/r/positions") ] serializers.Position.parse(*sub_data)
for sub_data in self._post("auth/r/positions")
]
def claim_position(self, def claim_position(
id: int, self, id: int, *, amount: Optional[Union[str, float, Decimal]] = None
*, ) -> Notification[PositionClaim]:
amount: Optional[Union[str, float, Decimal]] = None) -> Notification[PositionClaim]: return _Notification[PositionClaim](serializers.PositionClaim).parse(
return _Notification[PositionClaim](serializers.PositionClaim) \ *self._post("auth/w/position/claim", body={"id": id, "amount": amount})
.parse(*self._post("auth/w/position/claim", \ )
body={ "id": id, "amount": amount }))
def increase_position(self, def increase_position(
symbol: str, self, symbol: str, amount: Union[str, float, Decimal]
amount: Union[str, float, Decimal]) -> Notification[PositionIncrease]: ) -> Notification[PositionIncrease]:
return _Notification[PositionIncrease](serializers.PositionIncrease) \ return _Notification[PositionIncrease](serializers.PositionIncrease).parse(
.parse(*self._post("auth/w/position/increase", \ *self._post(
body={ "symbol": symbol, "amount": amount })) "auth/w/position/increase", body={"symbol": symbol, "amount": amount}
)
)
def get_increase_position_info(self, def get_increase_position_info(
symbol: str, self, symbol: str, amount: Union[str, float, Decimal]
amount: Union[str, float, Decimal]) -> PositionIncreaseInfo: ) -> PositionIncreaseInfo:
return serializers.PositionIncreaseInfo \ return serializers.PositionIncreaseInfo.parse(
.parse(*self._post("auth/r/position/increase/info", \ *self._post(
body={ "symbol": symbol, "amount": amount })) "auth/r/position/increase/info",
body={"symbol": symbol, "amount": amount},
)
)
def get_positions_history(self, def get_positions_history(
*, self,
start: Optional[str] = None, *,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[PositionHistory]: end: Optional[str] = None,
return [ serializers.PositionHistory.parse(*sub_data) \ limit: Optional[int] = None,
for sub_data in self._post("auth/r/positions/hist", \ ) -> List[PositionHistory]:
body={ "start": start, "end": end, "limit": limit }) ] 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, def get_positions_snapshot(
*, self,
start: Optional[str] = None, *,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[PositionSnapshot]: end: Optional[str] = None,
return [ serializers.PositionSnapshot.parse(*sub_data) \ limit: Optional[int] = None,
for sub_data in self._post("auth/r/positions/snap", \ ) -> List[PositionSnapshot]:
body={ "start": start, "end": end, "limit": limit }) ] 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, def get_positions_audit(
*, self,
ids: Optional[List[int]] = None, *,
start: Optional[str] = None, ids: Optional[List[int]] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[PositionAudit]: end: Optional[str] = None,
body = { limit: Optional[int] = None,
"ids": ids, "start": start, "end": end, ) -> List[PositionAudit]:
"limit": limit body = {"ids": ids, "start": start, "end": end, "limit": limit}
}
return [ serializers.PositionAudit.parse(*sub_data) \ return [
for sub_data in self._post("auth/r/positions/audit", body=body) ] serializers.PositionAudit.parse(*sub_data)
for sub_data in self._post("auth/r/positions/audit", body=body)
]
def set_derivative_position_collateral(self, def set_derivative_position_collateral(
symbol: str, self, symbol: str, collateral: Union[str, float, Decimal]
collateral: Union[str, float, Decimal]) -> DerivativePositionCollateral: ) -> DerivativePositionCollateral:
return serializers.DerivativePositionCollateral \ return serializers.DerivativePositionCollateral.parse(
.parse(*(self._post("auth/w/deriv/collateral/set", \ *(
body={ "symbol": symbol, "collateral": collateral })[0])) self._post(
"auth/w/deriv/collateral/set",
body={"symbol": symbol, "collateral": collateral},
)[0]
)
)
def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: def get_derivative_position_collateral_limits(
return serializers.DerivativePositionCollateralLimits \ self, symbol: str
.parse(*self._post("auth/calc/deriv/collateral/limit", body={ "symbol": symbol })) ) -> 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]: def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOffer]:
if symbol is None: if symbol is None:
endpoint = "auth/r/funding/offers" 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) \ return [
for sub_data in self._post(endpoint) ] serializers.FundingOffer.parse(*sub_data)
for sub_data in self._post(endpoint)
]
#pylint: disable-next=too-many-arguments def submit_funding_offer(
def submit_funding_offer(self, self,
type: str, type: str,
symbol: str, symbol: str,
amount: Union[str, float, Decimal], amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal], rate: Union[str, float, Decimal],
period: int, period: int,
*, *,
flags: Optional[int] = None) -> Notification[FundingOffer]: flags: Optional[int] = None,
) -> Notification[FundingOffer]:
body = { body = {
"type": type, "symbol": symbol, "amount": amount, "type": type,
"rate": rate, "period": period, "flags": flags "symbol": symbol,
"amount": amount,
"rate": rate,
"period": period,
"flags": flags,
} }
return _Notification[FundingOffer](serializers.FundingOffer) \ return _Notification[FundingOffer](serializers.FundingOffer).parse(
.parse(*self._post("auth/w/funding/offer/submit", body=body)) *self._post("auth/w/funding/offer/submit", body=body)
)
def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]:
return _Notification[FundingOffer](serializers.FundingOffer) \ return _Notification[FundingOffer](serializers.FundingOffer).parse(
.parse(*self._post("auth/w/funding/offer/cancel", body={ "id": id })) *self._post("auth/w/funding/offer/cancel", body={"id": id})
)
def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]:
return _Notification[Literal[None]](None) \ return _Notification[Literal[None]](None).parse(
.parse(*self._post("auth/w/funding/offer/cancel/all", body={ "currency": currency })) *self._post("auth/w/funding/offer/cancel/all", body={"currency": currency})
)
def submit_funding_close(self, id: int) -> Notification[Literal[None]]: def submit_funding_close(self, id: int) -> Notification[Literal[None]]:
return _Notification[Literal[None]](None) \ return _Notification[Literal[None]](None).parse(
.parse(*self._post("auth/w/funding/close", body={ "id": id })) *self._post("auth/w/funding/close", body={"id": id})
)
def toggle_auto_renew(self, def toggle_auto_renew(
status: bool, self,
currency: str, status: bool,
*, currency: str,
amount: Optional[str] = None, *,
rate: Optional[int] = None, amount: Optional[str] = None,
period: Optional[int] = None) -> Notification[FundingAutoRenew]: rate: Optional[int] = None,
period: Optional[int] = None,
) -> Notification[FundingAutoRenew]:
body = { body = {
"status": status, "currency": currency, "amount": amount, "status": status,
"rate": rate, "period": period "currency": currency,
"amount": amount,
"rate": rate,
"period": period,
} }
return _Notification[FundingAutoRenew](serializers.FundingAutoRenew) \ return _Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(
.parse(*self._post("auth/w/funding/auto", body=body)) *self._post("auth/w/funding/auto", body=body)
)
def toggle_keep_funding(self, def toggle_keep_funding(
type: Literal["credit", "loan"], self,
*, type: Literal["credit", "loan"],
ids: Optional[List[int]] = None, *,
changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]: ids: Optional[List[int]] = None,
return _Notification[Literal[None]](None) \ changes: Optional[Dict[int, Literal[1, 2]]] = None,
.parse(*self._post("auth/w/funding/keep", \ ) -> Notification[Literal[None]]:
body={ "type": type, "id": ids, "changes": changes })) 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, def get_funding_offers_history(
*, self,
symbol: Optional[str] = None, *,
start: Optional[str] = None, symbol: Optional[str] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingOffer]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[FundingOffer]:
if symbol is None: if symbol is None:
endpoint = "auth/r/funding/offers/hist" 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) \ return [
for sub_data in self._post(endpoint, \ serializers.FundingOffer.parse(*sub_data)
body={ "start": start, "end": end, "limit": limit }) ] 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]: def get_funding_loans(self, *, symbol: Optional[str] = None) -> List[FundingLoan]:
if symbol is None: if symbol is None:
endpoint = "auth/r/funding/loans" 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) \ return [
for sub_data in self._post(endpoint) ] serializers.FundingLoan.parse(*sub_data)
for sub_data in self._post(endpoint)
]
def get_funding_loans_history(self, def get_funding_loans_history(
*, self,
symbol: Optional[str] = None, *,
start: Optional[str] = None, symbol: Optional[str] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingLoan]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[FundingLoan]:
if symbol is None: if symbol is None:
endpoint = "auth/r/funding/loans/hist" 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) \ return [
for sub_data in self._post(endpoint, \ serializers.FundingLoan.parse(*sub_data)
body={ "start": start, "end": end, "limit": limit }) ] 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: if symbol is None:
endpoint = "auth/r/funding/credits" 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) \ return [
for sub_data in self._post(endpoint) ] serializers.FundingCredit.parse(*sub_data)
for sub_data in self._post(endpoint)
]
def get_funding_credits_history(self, def get_funding_credits_history(
*, self,
symbol: Optional[str] = None, *,
start: Optional[str] = None, symbol: Optional[str] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingCredit]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[FundingCredit]:
if symbol is None: if symbol is None:
endpoint = "auth/r/funding/credits/hist" 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) \ return [
for sub_data in self._post(endpoint, \ serializers.FundingCredit.parse(*sub_data)
body={ "start": start, "end": end, "limit": limit }) ] for sub_data in self._post(
endpoint, body={"start": start, "end": end, "limit": limit}
)
]
def get_funding_trades_history(self, def get_funding_trades_history(
*, self,
symbol: Optional[str] = None, *,
sort: Optional[int] = None, symbol: Optional[str] = None,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingTrade]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[FundingTrade]:
if symbol is None: if symbol is None:
endpoint = "auth/r/funding/trades/hist" 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 = { body = {"sort": sort, "start": start, "end": end, "limit": limit}
"sort": sort, "start": start, "end": end,
"limit": limit }
return [ serializers.FundingTrade.parse(*sub_data) \ return [
for sub_data in self._post(endpoint, body=body) ] serializers.FundingTrade.parse(*sub_data)
for sub_data in self._post(endpoint, body=body)
]
def get_funding_info(self, key: str) -> FundingInfo: def get_funding_info(self, key: str) -> FundingInfo:
return serializers.FundingInfo \ return serializers.FundingInfo.parse(
.parse(*(self._post(f"auth/r/info/funding/{key}")[2])) *(self._post(f"auth/r/info/funding/{key}")[2])
)
#pylint: disable-next=too-many-arguments def transfer_between_wallets(
def transfer_between_wallets(self, self,
from_wallet: str, from_wallet: str,
to_wallet: str, to_wallet: str,
currency: str, currency: str,
currency_to: str, currency_to: str,
amount: Union[str, float, Decimal]) -> Notification[Transfer]: amount: Union[str, float, Decimal],
) -> Notification[Transfer]:
body = { body = {
"from": from_wallet, "to": to_wallet, "currency": currency, "from": from_wallet,
"currency_to": currency_to, "amount": amount "to": to_wallet,
"currency": currency,
"currency_to": currency_to,
"amount": amount,
} }
return _Notification[Transfer](serializers.Transfer) \ return _Notification[Transfer](serializers.Transfer).parse(
.parse(*self._post("auth/w/transfer", body=body)) *self._post("auth/w/transfer", body=body)
)
def submit_wallet_withdrawal(self, def submit_wallet_withdrawal(
wallet: str, self, wallet: str, method: str, address: str, amount: Union[str, float, Decimal]
method: str, ) -> Notification[Withdrawal]:
address: str,
amount: Union[str, float, Decimal]) -> Notification[Withdrawal]:
body = { body = {
"wallet": wallet, "method": method, "address": address, "wallet": wallet,
"amount": amount "method": method,
"address": address,
"amount": amount,
} }
return _Notification[Withdrawal](serializers.Withdrawal) \ return _Notification[Withdrawal](serializers.Withdrawal).parse(
.parse(*self._post("auth/w/withdraw", body=body)) *self._post("auth/w/withdraw", body=body)
)
def get_deposit_address(self, def get_deposit_address(
wallet: str, self, wallet: str, method: str, op_renew: bool = False
method: str, ) -> Notification[DepositAddress]:
op_renew: bool = False) -> Notification[DepositAddress]: return _Notification[DepositAddress](serializers.DepositAddress).parse(
return _Notification[DepositAddress](serializers.DepositAddress) \ *self._post(
.parse(*self._post("auth/w/deposit/address", \ "auth/w/deposit/address",
body={ "wallet": wallet, "method": method, "op_renew": op_renew })) body={"wallet": wallet, "method": method, "op_renew": op_renew},
)
)
def generate_deposit_invoice(self, def generate_deposit_invoice(
wallet: str, self, wallet: str, currency: str, amount: Union[str, float, Decimal]
currency: str, ) -> LightningNetworkInvoice:
amount: Union[str, float, Decimal]) -> LightningNetworkInvoice: return serializers.LightningNetworkInvoice.parse(
return serializers.LightningNetworkInvoice \ *self._post(
.parse(*self._post("auth/w/deposit/invoice", \ "auth/w/deposit/invoice",
body={ "wallet": wallet, "currency": currency, "amount": amount })) body={"wallet": wallet, "currency": currency, "amount": amount},
)
)
def get_movements(self, def get_movements(
*, self,
currency: Optional[str] = None, *,
start: Optional[str] = None, currency: Optional[str] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Movement]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[Movement]:
if currency is None: if currency is None:
endpoint = "auth/r/movements/hist" 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) \ return [
for sub_data in self._post(endpoint, \ serializers.Movement.parse(*sub_data)
body={ "start": start, "end": end, "limit": limit }) ] 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 decimal import Decimal
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
from bfxapi.rest.middleware import Middleware from bfxapi.rest.middleware import Middleware
from bfxapi.types import (
CurrencyConversion,
InvoicePage,
InvoiceStats,
InvoiceSubmission,
MerchantDeposit,
MerchantUnlinkedDeposit,
)
from bfxapi.types import \ _CustomerInfo = TypedDict(
InvoiceSubmission, \ "_CustomerInfo",
InvoicePage, \ {
InvoiceStats, \ "nationality": str,
CurrencyConversion, \ "resid_country": str,
MerchantDeposit, \ "resid_city": str,
MerchantUnlinkedDeposit "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): class RestMerchantEndpoints(Middleware):
#pylint: disable-next=too-many-arguments def submit_invoice(
def submit_invoice(self, self,
amount: Union[str, float, Decimal], amount: Union[str, float, Decimal],
currency: str, currency: str,
order_id: str, order_id: str,
customer_info: _CustomerInfo, customer_info: _CustomerInfo,
pay_currencies: List[str], pay_currencies: List[str],
*, *,
duration: Optional[int] = None, duration: Optional[int] = None,
webhook: Optional[str] = None, webhook: Optional[str] = None,
redirect_url: Optional[str] = None) -> InvoiceSubmission: redirect_url: Optional[str] = None,
) -> InvoiceSubmission:
body = { body = {
"amount": amount, "currency": currency, "orderId": order_id, "amount": amount,
"customerInfo": customer_info, "payCurrencies": pay_currencies, "duration": duration, "currency": currency,
"webhook": webhook, "redirectUrl": redirect_url "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) data = self._post("auth/w/ext/pay/invoice/create", body=body)
return InvoiceSubmission.parse(data) return InvoiceSubmission.parse(data)
def get_invoices(self, def get_invoices(
*, self,
id: Optional[str] = None, *,
start: Optional[str] = None, id: Optional[str] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[InvoiceSubmission]: end: Optional[str] = None,
body = { limit: Optional[int] = None,
"id": id, "start": start, "end": end, ) -> List[InvoiceSubmission]:
"limit": limit body = {"id": id, "start": start, "end": end, "limit": limit}
}
data = self._post("auth/r/ext/pay/invoices", body=body) 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, def get_invoices_paginated(
page: int = 1, self,
page_size: int = 10, page: int = 1,
sort: Literal["asc", "desc"] = "asc", page_size: int = 10,
sort_field: Literal["t", "amount", "status"] = "t", 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, status: Optional[
crypto: Optional[List[str]] = None, List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]
id: Optional[str] = None, ] = None,
order_id: Optional[str] = None) -> InvoicePage: fiat: Optional[List[str]] = None,
crypto: Optional[List[str]] = None,
id: Optional[str] = None,
order_id: Optional[str] = None,
) -> InvoicePage:
body = { body = {
"page": page, "pageSize": page_size, "sort": sort, "page": page,
"sortField": sort_field, "status": status, "fiat": fiat, "pageSize": page_size,
"crypto": crypto, "id": id, "orderId": order_id "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) data = self._post("auth/r/ext/pay/invoices/paginated", body=body)
return InvoicePage.parse(data) return InvoicePage.parse(data)
def get_invoice_count_stats(self, def get_invoice_count_stats(
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str
format: str) -> List[InvoiceStats]: ) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in \ return [
self._post("auth/r/ext/pay/invoice/stats/count", \ InvoiceStats(**sub_data)
body={ "status": status, "format": format }) ] for sub_data in self._post(
"auth/r/ext/pay/invoice/stats/count",
body={"status": status, "format": format},
)
]
def get_invoice_earning_stats(self, def get_invoice_earning_stats(
currency: str, self, currency: str, format: str
format: str) -> List[InvoiceStats]: ) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in \ return [
self._post("auth/r/ext/pay/invoice/stats/earning", \ InvoiceStats(**sub_data)
body={ "currency": currency, "format": format }) ] for sub_data in self._post(
"auth/r/ext/pay/invoice/stats/earning",
body={"currency": currency, "format": format},
)
]
def complete_invoice(self, def complete_invoice(
id: str, self,
pay_currency: str, id: str,
*, pay_currency: str,
deposit_id: Optional[int] = None, *,
ledger_id: Optional[int] = None) -> InvoiceSubmission: deposit_id: Optional[int] = None,
ledger_id: Optional[int] = None,
) -> InvoiceSubmission:
body = { body = {
"id": id, "payCcy": pay_currency, "depositId": deposit_id, "id": id,
"ledgerId": ledger_id "payCcy": pay_currency,
"depositId": deposit_id,
"ledgerId": ledger_id,
} }
data = self._post("auth/w/ext/pay/invoice/complete", body=body) data = self._post("auth/w/ext/pay/invoice/complete", body=body)
@@ -115,65 +142,66 @@ class RestMerchantEndpoints(Middleware):
return InvoiceSubmission.parse(data) return InvoiceSubmission.parse(data)
def expire_invoice(self, id: str) -> InvoiceSubmission: def expire_invoice(self, id: str) -> InvoiceSubmission:
body = { "id": id } body = {"id": id}
data = self._post("auth/w/ext/pay/invoice/expire", body=body) data = self._post("auth/w/ext/pay/invoice/expire", body=body)
return InvoiceSubmission.parse(data) return InvoiceSubmission.parse(data)
def get_currency_conversion_list(self) -> List[CurrencyConversion]: def get_currency_conversion_list(self) -> List[CurrencyConversion]:
return [ CurrencyConversion(**sub_data) \ return [
for sub_data in self._post("auth/r/ext/pay/settings/convert/list") ] CurrencyConversion(**sub_data)
for sub_data in self._post("auth/r/ext/pay/settings/convert/list")
]
def add_currency_conversion(self, def add_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool:
base_ccy: str, return bool(
convert_ccy: str) -> bool: self._post(
return bool(self._post("auth/w/ext/pay/settings/convert/create", \ "auth/w/ext/pay/settings/convert/create",
body={ "baseCcy": base_ccy, "convertCcy": convert_ccy })) body={"baseCcy": base_ccy, "convertCcy": convert_ccy},
)
)
def remove_currency_conversion(self, def remove_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool:
base_ccy: str, return bool(
convert_ccy: str) -> bool: self._post(
return bool(self._post("auth/w/ext/pay/settings/convert/remove", \ "auth/w/ext/pay/settings/convert/remove",
body={ "baseCcy": base_ccy, "convertCcy": convert_ccy })) body={"baseCcy": base_ccy, "convertCcy": convert_ccy},
)
)
def set_merchant_settings(self, def set_merchant_settings(self, key: str, val: Any) -> bool:
key: str, return bool(
val: Any) -> bool: self._post("auth/w/ext/pay/settings/set", body={"key": key, "val": val})
return bool(self._post("auth/w/ext/pay/settings/set", \ )
body={ "key": key, "val": val }))
def get_merchant_settings(self, key: str) -> Any: 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(
def list_merchant_settings(self, keys: List[str] = []) -> Dict[str, Any]: self, keys: Optional[List[str]] = None
return self._post("auth/r/ext/pay/settings/list", body={ "keys": keys }) ) -> Dict[str, Any]:
return self._post("auth/r/ext/pay/settings/list", body={"keys": keys or []})
def get_deposits(self, def get_deposits(
start: int, self,
to: int, start: int,
*, to: int,
ccy: Optional[str] = None, *,
unlinked: Optional[bool] = None) -> List[MerchantDeposit]: ccy: Optional[str] = None,
body = { unlinked: Optional[bool] = None,
"from": start, "to": to, "ccy": ccy, ) -> List[MerchantDeposit]:
"unlinked": unlinked body = {"from": start, "to": to, "ccy": ccy, "unlinked": unlinked}
}
data = self._post("auth/r/ext/pay/deposits", body=body) 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, def get_unlinked_deposits(
ccy: str, self, ccy: str, *, start: Optional[int] = None, end: Optional[int] = None
*, ) -> List[MerchantUnlinkedDeposit]:
start: Optional[int] = None, body = {"ccy": ccy, "start": start, "end": end}
end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]:
body = {
"ccy": ccy, "start": start, "end": end
}
data = self._post("/auth/r/ext/pay/deposits/unlinked", body=body) 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 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 ..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): class RestPublicEndpoints(Middleware):
def conf(self, config: str) -> Any: def conf(self, config: str) -> Any:
return self._get(f"conf/{config}")[0] return self._get(f"conf/{config}")[0]
@@ -23,35 +35,47 @@ class RestPublicEndpoints(Middleware):
def get_platform_status(self) -> PlatformStatus: def get_platform_status(self) -> PlatformStatus:
return serializers.PlatformStatus.parse(*self._get("platform/status")) return serializers.PlatformStatus.parse(*self._get("platform/status"))
def get_tickers(self, symbols: List[str]) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]: def get_tickers(
data = self._get("tickers", params={ "symbols": ",".join(symbols) }) 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 } parsers = {
"t": serializers.TradingPairTicker.parse,
return { "f": serializers.FundingCurrencyTicker.parse,
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]: 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": if isinstance(symbols, str) and symbols == "ALL":
return { return {
symbol: cast(TradingPairTicker, sub_data) symbol: cast(TradingPairTicker, sub_data)
for symbol, sub_data in self.get_tickers([ "ALL" ]).items() for symbol, sub_data in self.get_tickers(["ALL"]).items()
if symbol.startswith("t") if symbol.startswith("t")
} }
data = self.get_tickers(list(symbols)) data = self.get_tickers(list(symbols))
return cast(Dict[str, TradingPairTicker], data) 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": if isinstance(symbols, str) and symbols == "ALL":
return { return {
symbol: cast(FundingCurrencyTicker, sub_data) symbol: cast(FundingCurrencyTicker, sub_data)
for symbol, sub_data in self.get_tickers([ "ALL" ]).items() for symbol, sub_data in self.get_tickers(["ALL"]).items()
if symbol.startswith("f") if symbol.startswith("f")
} }
data = self.get_tickers(list(symbols)) data = self.get_tickers(list(symbols))
@@ -64,230 +88,292 @@ class RestPublicEndpoints(Middleware):
def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker: def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker:
return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{symbol}")) return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{symbol}"))
def get_tickers_history(self, def get_tickers_history(
symbols: List[str], self,
*, symbols: List[str],
start: Optional[str] = None, *,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[TickersHistory]: end: Optional[str] = None,
return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._get("tickers/hist", params={ limit: Optional[int] = None,
"symbols": ",".join(symbols), ) -> List[TickersHistory]:
"start": start, "end": end, return [
"limit": limit 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, def get_t_trades(
pair: str, self,
*, pair: str,
limit: Optional[int] = None, *,
start: Optional[str] = None, limit: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
sort: Optional[int] = None) -> List[TradingPairTrade]: end: Optional[str] = None,
params = { "limit": limit, "start": start, "end": end, "sort": sort } sort: Optional[int] = None,
) -> List[TradingPairTrade]:
params = {"limit": limit, "start": start, "end": end, "sort": sort}
data = self._get(f"trades/{pair}/hist", params=params) 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, def get_f_trades(
currency: str, self,
*, currency: str,
limit: Optional[int] = None, *,
start: Optional[str] = None, limit: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
sort: Optional[int] = None) -> List[FundingCurrencyTrade]: end: Optional[str] = None,
params = { "limit": limit, "start": start, "end": end, "sort": sort } sort: Optional[int] = None,
) -> List[FundingCurrencyTrade]:
params = {"limit": limit, "start": start, "end": end, "sort": sort}
data = self._get(f"trades/{currency}/hist", params=params) 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, def get_t_book(
pair: str, self,
precision: Literal["P0", "P1", "P2", "P3", "P4"], pair: str,
*, precision: Literal["P0", "P1", "P2", "P3", "P4"],
len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: *,
return [ serializers.TradingPairBook.parse(*sub_data) \ len: Optional[Literal[1, 25, 100]] = None,
for sub_data in self._get(f"book/{pair}/{precision}", params={ "len": len }) ] ) -> 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, def get_f_book(
currency: str, self,
precision: Literal["P0", "P1", "P2", "P3", "P4"], currency: str,
*, precision: Literal["P0", "P1", "P2", "P3", "P4"],
len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: *,
return [ serializers.FundingCurrencyBook.parse(*sub_data) \ len: Optional[Literal[1, 25, 100]] = None,
for sub_data in self._get(f"book/{currency}/{precision}", params={ "len": len }) ] ) -> 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, def get_t_raw_book(
pair: str, self, pair: str, *, len: Optional[Literal[1, 25, 100]] = None
*, ) -> List[TradingPairRawBook]:
len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: return [
return [ serializers.TradingPairRawBook.parse(*sub_data) \ serializers.TradingPairRawBook.parse(*sub_data)
for sub_data in self._get(f"book/{pair}/R0", params={ "len": len }) ] for sub_data in self._get(f"book/{pair}/R0", params={"len": len})
]
def get_f_raw_book(self, def get_f_raw_book(
currency: str, self, currency: str, *, len: Optional[Literal[1, 25, 100]] = None
*, ) -> List[FundingCurrencyRawBook]:
len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: return [
return [ serializers.FundingCurrencyRawBook.parse(*sub_data) \ serializers.FundingCurrencyRawBook.parse(*sub_data)
for sub_data in self._get(f"book/{currency}/R0", params={ "len": len }) ] for sub_data in self._get(f"book/{currency}/R0", params={"len": len})
]
def get_stats_hist(self, def get_stats_hist(
resource: str, self,
*, resource: str,
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Statistic]: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } limit: Optional[int] = None,
) -> List[Statistic]:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"stats1/{resource}/hist", params=params) 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, def get_stats_last(
resource: str, self,
*, resource: str,
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> Statistic: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } limit: Optional[int] = None,
) -> Statistic:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"stats1/{resource}/last", params=params) data = self._get(f"stats1/{resource}/last", params=params)
return serializers.Statistic.parse(*data) return serializers.Statistic.parse(*data)
def get_candles_hist(self, def get_candles_hist(
symbol: str, self,
tf: str = "1m", symbol: str,
*, tf: str = "1m",
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Candle]: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } 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) 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, def get_candles_last(
symbol: str, self,
tf: str = "1m", symbol: str,
*, tf: str = "1m",
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> Candle: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } 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) data = self._get(f"candles/trade:{tf}:{symbol}/last", params=params)
return serializers.Candle.parse(*data) 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": if keys == "ALL":
params = { "keys": "ALL" } params = {"keys": "ALL"}
else: params = { "keys": ",".join(keys) } else:
params = {"keys": ",".join(keys)}
data = self._get("status/deriv", params=params) data = self._get("status/deriv", params=params)
return { return {
key: serializers.DerivativesStatus.parse(*sub_data) key: serializers.DerivativesStatus.parse(*sub_data)
for sub_data in data for sub_data in data
if (key := sub_data.pop(0)) if (key := sub_data.pop(0))
} }
def get_derivatives_status_history(self, def get_derivatives_status_history(
key: str, self,
*, key: str,
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[DerivativesStatus]: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } 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) 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, def get_liquidations(
*, self,
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Liquidation]: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } limit: Optional[int] = None,
) -> List[Liquidation]:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get("liquidations/hist", params=params) 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, def get_seed_candles(
symbol: str, self,
tf: str = "1m", symbol: str,
*, tf: str = "1m",
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Candle]: end: Optional[str] = None,
limit: Optional[int] = None,
) -> List[Candle]:
params = {"sort": sort, "start": start, "end": end, "limit": limit} params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params) 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, def get_leaderboards_hist(
resource: str, self,
*, resource: str,
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[Leaderboard]: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } limit: Optional[int] = None,
) -> List[Leaderboard]:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"rankings/{resource}/hist", params=params) 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, def get_leaderboards_last(
resource: str, self,
*, resource: str,
sort: Optional[int] = None, *,
start: Optional[str] = None, sort: Optional[int] = None,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> Leaderboard: end: Optional[str] = None,
params = { "sort": sort, "start": start, "end": end, "limit": limit } limit: Optional[int] = None,
) -> Leaderboard:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._get(f"rankings/{resource}/last", params=params) data = self._get(f"rankings/{resource}/last", params=params)
return serializers.Leaderboard.parse(*data) return serializers.Leaderboard.parse(*data)
def get_funding_stats(self, def get_funding_stats(
symbol: str, self,
*, symbol: str,
start: Optional[str] = None, *,
end: Optional[str] = None, start: Optional[str] = None,
limit: Optional[int] = None) -> List[FundingStatistic]: end: Optional[str] = None,
params = { "start": start, "end": end, "limit": limit } limit: Optional[int] = None,
) -> List[FundingStatistic]:
params = {"start": start, "end": end, "limit": limit}
data = self._get(f"funding/stats/{symbol}/hist", params=params) 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: def get_pulse_profile_details(self, nickname: str) -> PulseProfile:
return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}")) return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}"))
def get_pulse_message_history(self, def get_pulse_message_history(
*, self, *, end: Optional[str] = None, limit: Optional[int] = None
end: Optional[str] = None, ) -> List[PulseMessage]:
limit: Optional[int] = None) -> List[PulseMessage]:
messages = [] 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] sub_data[18] = sub_data[18][0]
message = serializers.PulseMessage.parse(*sub_data) message = serializers.PulseMessage.parse(*sub_data)
messages.append(message) messages.append(message)
return messages return messages
def get_trading_market_average_price(self, def get_trading_market_average_price(
symbol: str, self,
amount: Union[str, float, Decimal], symbol: str,
*, amount: Union[str, float, Decimal],
price_limit: Optional[Union[str, float, Decimal]] = None *,
) -> TradingMarketAveragePrice: price_limit: Optional[Union[str, float, Decimal]] = None,
return serializers.TradingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={ ) -> TradingMarketAveragePrice:
"symbol": symbol, "amount": amount, "price_limit": price_limit 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, def get_funding_market_average_price(
symbol: str, self,
amount: Union[str, float, Decimal], symbol: str,
period: int, amount: Union[str, float, Decimal],
*, period: int,
rate_limit: Optional[Union[str, float, Decimal]] = None *,
) -> FundingMarketAveragePrice: rate_limit: Optional[Union[str, float, Decimal]] = None,
return serializers.FundingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={ ) -> FundingMarketAveragePrice:
"symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit 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: 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 from bfxapi.exceptions import BfxBaseException
class NotFoundError(BfxBaseException): class NotFoundError(BfxBaseException):
pass pass
class RequestParametersError(BfxBaseException): class RequestParametersError(BfxBaseException):
pass pass
class UnknownGenericError(BfxBaseException): class UnknownGenericError(BfxBaseException):
pass 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 enum import IntEnum
from http import HTTPStatus 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_decoder import JSONDecoder
from ..._utils.json_encoder import JSONEncoder
from ...exceptions import InvalidCredentialError
from ..exceptions import NotFoundError, RequestParametersError, UnknownGenericError
if TYPE_CHECKING: if TYPE_CHECKING:
from requests.sessions import _Params from requests.sessions import _Params
class _Error(IntEnum): class _Error(IntEnum):
ERR_UNK = 10000 ERR_UNK = 10000
ERR_GENERIC = 10001 ERR_GENERIC = 10001
ERR_PARAMS = 10020 ERR_PARAMS = 10020
ERR_AUTH_FAIL = 10100 ERR_AUTH_FAIL = 10100
class Middleware: class Middleware:
TIMEOUT = 30 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 self.host, self.api_key, self.api_secret = host, api_key, api_secret
def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None): def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None):
assert isinstance(self.api_key, str) and isinstance(self.api_secret, str), \ assert isinstance(self.api_key, str) and isinstance(
"API_KEY and API_SECRET must be both str to call __build_authentication_headers" self.api_secret, str
), "API_KEY and API_SECRET must be both strings"
nonce = str(round(time.time() * 1_000_000)) nonce = str(round(time.time() * 1_000_000))
if data is None: if data is None:
path = f"/api/v2/{endpoint}{nonce}" 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( signature = hmac.new(
self.api_secret.encode("utf8"), self.api_secret.encode("utf8"), path.encode("utf8"), hashlib.sha384
path.encode("utf8"),
hashlib.sha384
).hexdigest() ).hexdigest()
return { return {
"bfx-nonce": nonce, "bfx-nonce": nonce,
"bfx-signature": signature, "bfx-signature": signature,
"bfx-apikey": self.api_key "bfx-apikey": self.api_key,
} }
def _get(self, endpoint: str, params: Optional["_Params"] = None) -> Any: def _get(self, endpoint: str, params: Optional["_Params"] = None) -> Any:
response = requests.get( response = requests.get(
url=f"{self.host}/{endpoint}", url=f"{self.host}/{endpoint}", params=params, timeout=Middleware.TIMEOUT
params=params,
timeout=Middleware.TIMEOUT
) )
if response.status_code == HTTPStatus.NOT_FOUND: if response.status_code == HTTPStatus.NOT_FOUND:
@@ -63,30 +66,43 @@ class Middleware:
if len(data) and data[0] == "error": if len(data) and data[0] == "error":
if data[1] == _Error.ERR_PARAMS: if data[1] == _Error.ERR_PARAMS:
raise RequestParametersError("The request was rejected with the " \ raise RequestParametersError(
f"following parameter error: <{data[2]}>") "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: if (
raise UnknownGenericError("The server replied to the request with " \ data[1] is None
f"a generic error with message: <{data[2]}>.") 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 return data
def _post(self, endpoint: str, params: Optional["_Params"] = None, def _post(
body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: 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 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: 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( response = requests.post(
url=f"{self.host}/{endpoint}", url=f"{self.host}/{endpoint}",
params=params, params=params,
data=data, data=data,
headers=headers, headers=headers,
timeout=Middleware.TIMEOUT timeout=Middleware.TIMEOUT,
) )
if response.status_code == HTTPStatus.NOT_FOUND: 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 isinstance(data, list) and len(data) and data[0] == "error":
if data[1] == _Error.ERR_PARAMS: if data[1] == _Error.ERR_PARAMS:
raise RequestParametersError("The request was rejected with the " \ raise RequestParametersError(
f"following parameter error: <{data[2]}>") "The request was rejected with the "
f"following parameter error: <{data[2]}>"
)
if data[1] == _Error.ERR_AUTH_FAIL: 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: if (
raise UnknownGenericError("The server replied to the request with " \ data[1] is None
f"a generic error with message: <{data[2]}>.") 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 return data

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
from typing import Type, Generic, TypeVar, Iterable, \ from typing import Any, Dict, Generic, Iterable, List, Tuple, Type, TypeVar, cast
Dict, List, Tuple, Any, cast
T = TypeVar("T", bound="_Type") T = TypeVar("T", bound="_Type")
def compose(*decorators): def compose(*decorators):
def wrapper(function): def wrapper(function):
for decorator in reversed(decorators): for decorator in reversed(decorators):
@@ -11,30 +11,38 @@ def compose(*decorators):
return wrapper return wrapper
def partial(cls): def partial(cls):
def __init__(self, **kwargs): def __init__(self, **kwargs):
for annotation in self.__annotations__.keys(): for annotation in self.__annotations__.keys():
if annotation not in kwargs: if annotation not in kwargs:
self.__setattr__(annotation, None) self.__setattr__(annotation, None)
else: self.__setattr__(annotation, kwargs[annotation]) else:
self.__setattr__(annotation, kwargs[annotation])
kwargs.pop(annotation, None) kwargs.pop(annotation, None)
if len(kwargs) != 0: 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__ cls.__init__ = __init__
return cls return cls
class _Type: class _Type:
""" """
Base class for any dataclass serializable by the _Serializer generic class. Base class for any dataclass serializable by the _Serializer generic class.
""" """
class _Serializer(Generic[T]): class _Serializer(Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str], def __init__(
*, flat: bool = False): self, name: str, klass: Type[_Type], labels: List[str], *, flat: bool = False
):
self.name, self.klass, self.__labels, self.__flat = name, klass, labels, flat self.name, self.klass, self.__labels, self.__flat = name, klass, labels, flat
def _serialize(self, *args: Any) -> Iterable[Tuple[str, Any]]: def _serialize(self, *args: Any) -> Iterable[Tuple[str, Any]]:
@@ -42,8 +50,10 @@ class _Serializer(Generic[T]):
args = tuple(_Serializer.__flatten(list(args))) args = tuple(_Serializer.__flatten(list(args)))
if len(self.__labels) > len(args): if len(self.__labels) > len(args):
raise AssertionError(f"{self.name} -> <labels> and <*args> " \ raise AssertionError(
"arguments should contain the same amount of elements.") f"{self.name} -> <labels> and <*args> "
"arguments should contain the same amount of elements."
)
for index, label in enumerate(self.__labels): for index, label in enumerate(self.__labels):
if label != "_PLACEHOLDER": if label != "_PLACEHOLDER":
@@ -53,7 +63,7 @@ class _Serializer(Generic[T]):
return cast(T, self.klass(**dict(self._serialize(*values)))) return cast(T, self.klass(**dict(self._serialize(*values))))
def get_labels(self) -> List[str]: 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 @classmethod
def __flatten(cls, array: List[Any]) -> List[Any]: def __flatten(cls, array: List[Any]) -> List[Any]:
@@ -65,10 +75,17 @@ class _Serializer(Generic[T]):
return array[:1] + cls.__flatten(array[1:]) return array[:1] + cls.__flatten(array[1:])
class _RecursiveSerializer(_Serializer, Generic[T]): class _RecursiveSerializer(_Serializer, Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str], def __init__(
*, serializers: Dict[str, _Serializer[Any]], self,
flat: bool = False): name: str,
klass: Type[_Type],
labels: List[str],
*,
serializers: Dict[str, _Serializer[Any]],
flat: bool = False,
):
super().__init__(name, klass, labels, flat=flat) super().__init__(name, klass, labels, flat=flat)
self.serializers = serializers self.serializers = serializers
@@ -82,15 +99,21 @@ class _RecursiveSerializer(_Serializer, Generic[T]):
return cast(T, self.klass(**serialization)) 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], def generate_labeler_serializer(
*, serializers: Dict[str, _Serializer[Any]], name: str, klass: Type[T], labels: List[str], *, flat: bool = False
flat: bool = False ) -> _Serializer[T]:
) -> _RecursiveSerializer[T]: return _Serializer[T](name, klass, labels, flat=flat)
return _RecursiveSerializer[T](name, klass, labels, \
serializers=serializers, 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 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") T = TypeVar("T")
@dataclass @dataclass
class Notification(_Type, Generic[T]): class Notification(_Type, Generic[T]):
mts: int mts: int
@@ -14,16 +16,30 @@ class Notification(_Type, Generic[T]):
status: str status: str
text: 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) super().__init__("Notification", Notification, _Notification.__LABELS)
self.serializer, self.is_iterable = serializer, is_iterable self.serializer, self.is_iterable = serializer, is_iterable
def parse(self, *values: Any) -> Notification[T]: 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): if isinstance(self.serializer, _Serializer):
data = cast(List[Any], notification.data) data = cast(List[Any], notification.data)
@@ -33,6 +49,9 @@ class _Notification(_Serializer, Generic[T]):
data = data[0] data = data[0]
notification.data = self.serializer.parse(*data) 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 return notification

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,15 @@
from typing import \ import hashlib
TypeVar, Callable, Awaitable, \ import hmac
List, Dict, Optional, \ import json
Any, cast from abc import ABC, abstractmethod
# pylint: disable-next=wrong-import-order
from typing_extensions import \
ParamSpec, Concatenate
from abc import \
ABC, abstractmethod
from functools import wraps
from datetime import datetime 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 websockets.client import WebSocketClientProtocol
from bfxapi.websocket.exceptions import \ from bfxapi.websocket.exceptions import ActionRequiresAuthentication, ConnectionNotOpen
ConnectionNotOpen, ActionRequiresAuthentication
_S = TypeVar("_S", bound="Connection") _S = TypeVar("_S", bound="Connection")
@@ -27,6 +17,7 @@ _R = TypeVar("_R")
_P = ParamSpec("_P") _P = ParamSpec("_P")
class Connection(ABC): class Connection(ABC):
_HEARTBEAT = "hb" _HEARTBEAT = "hb"
@@ -39,8 +30,7 @@ class Connection(ABC):
@property @property
def open(self) -> bool: def open(self) -> bool:
return self.__protocol is not None and \ return self.__protocol is not None and self.__protocol.open
self.__protocol.open
@property @property
def authentication(self) -> bool: def authentication(self) -> bool:
@@ -55,12 +45,11 @@ class Connection(ABC):
self.__protocol = protocol self.__protocol = protocol
@abstractmethod @abstractmethod
async def start(self) -> None: async def start(self) -> None: ...
...
@staticmethod @staticmethod
def _require_websocket_connection( def _require_websocket_connection(
function: Callable[Concatenate[_S, _P], Awaitable[_R]] function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]: ) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function) @wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R: async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
@@ -73,13 +62,15 @@ class Connection(ABC):
@staticmethod @staticmethod
def _require_websocket_authentication( def _require_websocket_authentication(
function: Callable[Concatenate[_S, _P], Awaitable[_R]] function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]: ) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function) @wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R: async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
if not self.authentication: if not self.authentication:
raise ActionRequiresAuthentication("To perform this action you need to " \ raise ActionRequiresAuthentication(
"authenticate using your API_KEY and API_SECRET.") "To perform this action you need to "
"authenticate using your API_KEY and API_SECRET."
)
internal = Connection._require_websocket_connection(function) internal = Connection._require_websocket_connection(function)
@@ -89,12 +80,13 @@ class Connection(ABC):
@staticmethod @staticmethod
def _get_authentication_message( def _get_authentication_message(
api_key: str, api_key: str, api_secret: str, filters: Optional[List[str]] = None
api_secret: str,
filters: Optional[List[str]] = None
) -> str: ) -> str:
message: Dict[str, Any] = \ message: Dict[str, Any] = {
{ "event": "auth", "filter": filters, "apiKey": api_key } "event": "auth",
"filter": filters,
"apiKey": api_key,
}
message["authNonce"] = round(datetime.now().timestamp() * 1_000_000) message["authNonce"] = round(datetime.now().timestamp() * 1_000_000)
@@ -103,7 +95,7 @@ class Connection(ABC):
auth_sig = hmac.new( auth_sig = hmac.new(
key=api_secret.encode("utf8"), key=api_secret.encode("utf8"),
msg=message["authPayload"].encode("utf8"), msg=message["authPayload"].encode("utf8"),
digestmod=hashlib.sha384 digestmod=hashlib.sha384,
) )
message["authSig"] = auth_sig.hexdigest() 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 asyncio import AbstractEventLoop
from collections import defaultdict
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
from pyee.asyncio import AsyncIOEventEmitter from pyee.asyncio import AsyncIOEventEmitter
from bfxapi.websocket.exceptions import UnknownEventError from bfxapi.websocket.exceptions import UnknownEventError
@@ -12,57 +9,86 @@ from bfxapi.websocket.exceptions import UnknownEventError
_Handler = TypeVar("_Handler", bound=Callable[..., None]) _Handler = TypeVar("_Handler", bound=Callable[..., None])
_ONCE_PER_CONNECTION = [ _ONCE_PER_CONNECTION = [
"open", "authenticated", "order_snapshot", "open",
"position_snapshot", "funding_offer_snapshot", "funding_credit_snapshot", "authenticated",
"funding_loan_snapshot", "wallet_snapshot" "order_snapshot",
"position_snapshot",
"funding_offer_snapshot",
"funding_credit_snapshot",
"funding_loan_snapshot",
"wallet_snapshot",
] ]
_ONCE_PER_SUBSCRIPTION = [ _ONCE_PER_SUBSCRIPTION = [
"subscribed", "t_trades_snapshot", "f_trades_snapshot", "subscribed",
"t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "t_trades_snapshot",
"f_raw_book_snapshot", "candles_snapshot" "f_trades_snapshot",
"t_book_snapshot",
"f_book_snapshot",
"t_raw_book_snapshot",
"f_raw_book_snapshot",
"candles_snapshot",
] ]
_COMMON = [ _COMMON = [
"disconnected", "t_ticker_update", "f_ticker_update", "disconnected",
"t_trade_execution", "t_trade_execution_update", "f_trade_execution", "t_ticker_update",
"f_trade_execution_update", "t_book_update", "f_book_update", "f_ticker_update",
"t_raw_book_update", "f_raw_book_update", "candles_update", "t_trade_execution",
"derivatives_status_update", "liquidation_feed_update", "checksum", "t_trade_execution_update",
"order_new", "order_update", "order_cancel", "f_trade_execution",
"position_new", "position_update", "position_close", "f_trade_execution_update",
"funding_offer_new", "funding_offer_update", "funding_offer_cancel", "t_book_update",
"funding_credit_new", "funding_credit_update", "funding_credit_close", "f_book_update",
"funding_loan_new", "funding_loan_update", "funding_loan_close", "t_raw_book_update",
"trade_execution", "trade_execution_update", "wallet_update", "f_raw_book_update",
"notification", "on-req-notification", "ou-req-notification", "candles_update",
"oc-req-notification", "fon-req-notification", "foc-req-notification" "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): class BfxEventEmitter(AsyncIOEventEmitter):
_EVENTS = _ONCE_PER_CONNECTION + \ _EVENTS = _ONCE_PER_CONNECTION + _ONCE_PER_SUBSCRIPTION + _COMMON
_ONCE_PER_SUBSCRIPTION + \
_COMMON
def __init__(self, loop: Optional[AbstractEventLoop] = None) -> None: def __init__(self, loop: Optional[AbstractEventLoop] = None) -> None:
super().__init__(loop) super().__init__(loop)
self._connection: List[str] = [ ] self._connection: List[str] = []
self._subscriptions: Dict[str, List[str]] = \ self._subscriptions: Dict[str, List[str]] = defaultdict(lambda: [])
defaultdict(lambda: [ ])
def emit( def emit(self, event: str, *args: Any, **kwargs: Any) -> bool:
self,
event: str,
*args: Any,
**kwargs: Any
) -> bool:
if event in _ONCE_PER_CONNECTION: if event in _ONCE_PER_CONNECTION:
if event in self._connection: if event in self._connection:
return self._has_listeners(event) return self._has_listeners(event)
self._connection += [ event ] self._connection += [event]
if event in _ONCE_PER_SUBSCRIPTION: if event in _ONCE_PER_SUBSCRIPTION:
sub_id = args[0]["sub_id"] sub_id = args[0]["sub_id"]
@@ -70,7 +96,7 @@ class BfxEventEmitter(AsyncIOEventEmitter):
if event in self._subscriptions[sub_id]: if event in self._subscriptions[sub_id]:
return self._has_listeners(event) return self._has_listeners(event)
self._subscriptions[sub_id] += [ event ] self._subscriptions[sub_id] += [event]
return super().emit(event, *args, **kwargs) return super().emit(event, *args, **kwargs)
@@ -78,8 +104,10 @@ class BfxEventEmitter(AsyncIOEventEmitter):
self, event: str, f: Optional[_Handler] = None self, event: str, f: Optional[_Handler] = None
) -> Union[_Handler, Callable[[_Handler], _Handler]]: ) -> Union[_Handler, Callable[[_Handler], _Handler]]:
if event not in BfxEventEmitter._EVENTS: if event not in BfxEventEmitter._EVENTS:
raise UnknownEventError(f"Can't register to unknown event: <{event}> " + \ raise UnknownEventError(
"(to get a full list of available events see https://docs.bitfinex.com/).") 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) return super().on(event, f)

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@@ -8,10 +8,7 @@ from bfxapi.types import (
DerivativePositionCollateralLimits, DerivativePositionCollateralLimits,
) )
bfx = Client( bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
submit_order_notification = bfx.rest.auth.submit_order( submit_order_notification = bfx.rest.auth.submit_order(
type="LIMIT", symbol="tBTCF0:USTF0", amount="0.015", price="16700", lev=10 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 import Client
from bfxapi.types import FundingOffer, Notification from bfxapi.types import FundingOffer, Notification
bfx = Client( bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
# Submit a new funding offer # Submit a new funding offer
notification: Notification[FundingOffer] = bfx.rest.auth.submit_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 import Client
from bfxapi.types import Notification, Order from bfxapi.types import Notification, Order
bfx = Client( bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
# Submit a new order # Submit a new order
submit_order_notification: Notification[Order] = bfx.rest.auth.submit_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 import Client
from bfxapi.types import FundingLoan, Notification from bfxapi.types import FundingLoan, Notification
bfx = Client( bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
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") loans: List[FundingLoan] = bfx.rest.auth.get_funding_loans(symbol="fUSD")

View File

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

View File

@@ -5,10 +5,7 @@ import os
from bfxapi import Client from bfxapi import Client
from bfxapi.types import InvoiceSubmission from bfxapi.types import InvoiceSubmission
bfx = Client( bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET"))
api_key=os.getenv("BFX_API_KEY"),
api_secret=os.getenv("BFX_API_SECRET")
)
customer_info = { customer_info = {
"nationality": "DE", "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 from distutils.core import setup
_version = { } from bfxapi._version import __version__
with open("bfxapi/_version.py", encoding="utf-8") as f:
#pylint: disable-next=exec-used
exec(f.read(), _version)
setup( setup(
name="bitfinex-api-py", name="bitfinex-api-py",
version=_version["__version__"], version=__version__,
description="Official Bitfinex Python API", 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", long_description_content_type="text/markdown",
url="https://github.com/bitfinexcom/bitfinex-api-py", url="https://github.com/bitfinexcom/bitfinex-api-py",
author="Bitfinex", author="Bitfinex",
@@ -18,12 +17,9 @@ setup(
license="Apache-2.0", license="Apache-2.0",
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Build Tools",
"License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
@@ -52,5 +48,5 @@ setup(
"requests~=2.28.1", "requests~=2.28.1",
"urllib3~=1.26.14", "urllib3~=1.26.14",
], ],
python_requires=">=3.8" python_requires=">=3.8",
) )